diff --git a/.config/git-merge-flow-config.jsonc b/.config/git-merge-flow-config.jsonc index f1070da2f95..13efaeba878 100644 --- a/.config/git-merge-flow-config.jsonc +++ b/.config/git-merge-flow-config.jsonc @@ -28,9 +28,9 @@ }, // Automate opening PRs to merge msbuild's vs18.0 (SDK 10.0.1xx) into vs18.3 (SDK 10.0.2xx, VS) "vs18.0": { - "MergeToBranch": "main" // update to flow through vs18.3 after we fork for release + "MergeToBranch": "vs18.3" }, - // MSBuild latest release to main + // Automate opening PRs to merge msbuild's vs18.3 (SDK 10.0.2xx) into main "vs18.3": { "MergeToBranch": "main" } diff --git a/.editorconfig b/.editorconfig index 99c82a0c47f..24cb45298a2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -415,6 +415,12 @@ dotnet_diagnostic.IDE0301.severity = suggestion dotnet_diagnostic.IDE0305.severity = suggestion dotnet_diagnostic.IDE0306.severity = suggestion +# Remove unnecessary nullable warning suppression +dotnet_diagnostic.IDE0370.severity = suggestion + +# Remove unnecessary 'unsafe' modifier +dotnet_diagnostic.IDE0380.severity = suggestion + # Temporarily disable SA1010 "Opening square brackets should not be preceded by a space" until https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 is fixed dotnet_diagnostic.SA1010.severity = none diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6949271d1d5..c880d138cf1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,8 +11,90 @@ New files should use nullable types but don't refactor aggressively existing cod Generate tests for new codepaths, and add tests for any bugs you fix. Use the existing test framework, which is xUnit with Shouldly assertions. Use Shouldly assertions for all assertions in modified code, even if the file is predominantly using xUnit assertions. +When making changes, check if related documentation exists in the `documentation/` folder (including `documentation/specs/`) and update it to reflect your changes. Keep documentation in sync with code changes, especially for telemetry, APIs, and architectural decisions. + Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. +## Performance Best Practices + +MSBuild is performance-critical infrastructure. Follow these patterns: + +### Switch Expressions for Dispatch Logic +Use tuple switch expressions for multi-condition dispatch instead of if-else chains: +```csharp +// GOOD: Clean, O(1) dispatch +return (c0, c1) switch +{ + ('C', 'S') => Category.CSharp, + ('F', 'S') => Category.FSharp, + ('V', 'B') when value.Length >= 3 && value[2] == 'C' => Category.VB, + _ => Category.Other +}; + +// AVOID: Verbose if-else chains +if (c0 == 'C' && c1 == 'S') return Category.CSharp; +else if (c0 == 'F' && c1 == 'S') return Category.FSharp; +// ... +``` + +### Range Pattern Matching +Use range patterns for numeric categorization: +```csharp +// GOOD: Clear and efficient +return errorNumber switch +{ + >= 3001 and <= 3999 => Category.Tasks, + >= 4001 and <= 4099 => Category.General, + >= 4100 and <= 4199 => Category.Evaluation, + _ => Category.Other +}; +``` + +### String Comparisons +- Use `StringComparer.OrdinalIgnoreCase` for case-insensitive HashSets/Dictionaries when the source data may vary in casing +- Use `char.ToUpperInvariant()` for single-character comparisons +- Use `ReadOnlySpan` and `Slice()` to avoid string allocations when parsing substrings +- Use `int.TryParse(span, out var result)` on .NET Core+ for allocation-free parsing + +### Inlining +Mark small, hot-path methods with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`: +```csharp +[MethodImpl(MethodImplOptions.AggressiveInlining)] +private static bool IsCompilerPrefix(string value) => ... +``` + +### Conditional Compilation for Framework Differences +Use `#if NET` for APIs that differ between .NET Framework and .NET Core: +```csharp +#if NET + return int.TryParse(span, out errorNumber); +#else + return int.TryParse(span.ToString(), out errorNumber); +#endif +``` + +### Immutable Collections +Choose the right immutable collection type based on usage pattern: + +**Build once, read many times** (most common in MSBuild): +- Use `ImmutableArray` instead of `ImmutableList` - significantly faster for read access +- Use `FrozenDictionary` instead of `ImmutableDictionary` - optimized for read-heavy scenarios + +**Build incrementally over time** (adding items one by one): +- Use `ImmutableList` and `ImmutableDictionary` - designed for efficient `Add` operations returning new collections + +```csharp +// GOOD: Build once from LINQ, then read many times +ImmutableArray items = source.Select(x => x.Name).ToImmutableArray(); +FrozenDictionary lookup = pairs.ToFrozenDictionary(x => x.Key, x => x.Value); + +// AVOID for read-heavy scenarios: +ImmutableList items = source.Select(x => x.Name).ToImmutableList(); +ImmutableDictionary lookup = pairs.ToImmutableDictionary(x => x.Key, x => x.Value); +``` + +Note: `ImmutableArray` is a value type. Use `IsDefault` property to check for uninitialized arrays, or use nullable `ImmutableArray?` with `.Value` to unwrap. + ## Working Effectively #### Bootstrap and Build the Repository diff --git a/.vsts-dotnet-ci.yml b/.vsts-dotnet-ci.yml index eb38f6d96a3..fc7940289f0 100644 --- a/.vsts-dotnet-ci.yml +++ b/.vsts-dotnet-ci.yml @@ -20,6 +20,14 @@ jobs: steps: - powershell: | $versionsFile = "eng/Versions.props" + + [xml]$xml = Get-Content $versionsFile + $finalVersionKind = $xml.Project.PropertyGroup.DotNetFinalVersionKind + if ($finalVersionKind -ne 'release') { + Write-Host "Since it is not released, skip the version bump check."; + return + } + $changedFiles = git diff --name-only HEAD HEAD~1 $changedVersionsFile = $changedFiles | Where-Object { $_ -eq $versionsFile } $isInitialCommit = $false diff --git a/Directory.Packages.props b/Directory.Packages.props index 2961c3bd5ff..2de8bea8769 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,11 +45,10 @@ + - - - - + + diff --git a/NuGet.config b/NuGet.config index c181d033061..45c16d21697 100644 --- a/NuGet.config +++ b/NuGet.config @@ -16,13 +16,11 @@ + + - - - - @@ -53,6 +51,12 @@ + + + + + + diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index 49e551d4279..982c50663a3 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -43,33 +43,3 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -------------------------------- - -Notice for OpenTelemetry .NET -------------------------------- -MSBuild.exe is distributed with OpenTelemetry .NET binaries. - -Copyright (c) OpenTelemetry Authors -Source: https://github.com/open-telemetry/opentelemetry-dotnet - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. - -------------------------------- - -Notice for Microsoft.VisualStudio.OpenTelemetry.* -------------------------------- -MSBuild.exe is distributed with Microsoft.VisualStudio.OpenTelemetry.* binaries. - -Project: Microsoft.VisualStudio.OpenTelemetry -Copyright: (c) Microsoft Corporation -License: https://visualstudio.microsoft.com/license-terms/mt736442/ \ No newline at end of file diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index fda53e27985..bf123ffd4c8 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -29,7 +29,10 @@ jobs: - output: artifactsDrop sourcePath: '$(Build.SourcesDirectory)\artifacts\official\OptProf\$(BuildConfiguration)\Data' dropServiceURI: 'https://devdiv.artifacts.visualstudio.com' - dropMetadataContainerName: 'ProfilingInputs/DevDiv/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber)' + buildNumber: 'ProfilingInputs/DevDiv/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber)' + toLowerCase: false + usePat: true + dropMetadataContainerName: 'DropMetadata-OptProf' condition: and(succeeded(), ${{ parameters.enableOptProf }}) # Publish bootstrapper info for OptProf data collection run to consume diff --git a/azure-pipelines/vs-insertion-experimental.yml b/azure-pipelines/vs-insertion-experimental.yml index a8acaa66478..5b05db87d14 100644 --- a/azure-pipelines/vs-insertion-experimental.yml +++ b/azure-pipelines/vs-insertion-experimental.yml @@ -24,6 +24,8 @@ parameters: displayName: 'Insertion Target Branch (select for manual insertion)' values: - main + - rel/d18.4 + - rel/d18.3 - rel/d18.0 - rel/d17.14 - rel/d17.13 diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 10542091113..76f7ff3a5d1 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -44,6 +44,7 @@ parameters: values: - auto - main + - rel/d18.4 - rel/d18.3 - rel/d18.0 - rel/d17.14 @@ -67,7 +68,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.3') }}: + ${{ if eq(variables['Build.SourceBranchName'], 'vs18.4') }}: + value: 'rel/d18.4' + ${{ elseif eq(variables['Build.SourceBranchName'], 'vs18.3') }}: value: 'rel/d18.3' ${{ elseif eq(variables['Build.SourceBranchName'], 'vs18.0') }}: value: 'rel/d18.0' diff --git a/documentation/High-level-overview.md b/documentation/High-level-overview.md index 4ee9aaa9e30..6cff44e8454 100644 --- a/documentation/High-level-overview.md +++ b/documentation/High-level-overview.md @@ -149,6 +149,15 @@ TaskHost can be opted-in via `TaskFactory="TaskHostFactory"` in the [`UsingTask` - If a task's source code is in the same repository that is being built, and the repository's build needs to use that task during the build process. Using a Task Host makes sure the DLLs are not locked at the end of the build (as MSBuild uses long living worker nodes that survives single build execution) - As an isolation mechanism - separating the execution from the engine execution process. +When `TaskHostFactory` is specified as the task factory, the task always runs out-of-process and short lived. See the below matrix: + +| Does TaskHost match executing MSBuild Runtime? | Is TaskHostFactory requested for the Task? | Expected task execution type | +| :-: | :-: | --- | +| ✅ | :x: | in-process execution | +| ✅ | ✅ | short-lived out-of-proc execution | +| :x: | ✅ | short-lived out-of-proc execution | +| :x: | :x: | long-lived out-of-proc execution | + ## Caches ### Project result cache The project Result Cache refers to the cache used by the scheduler that keeps the build results of already executed project. The result of a target is success, failure, and a list of items that succeeded. Beyond that, the `Returns` and `Outputs` attributes from targets are also serialized with the build result, as to be used by other targets for their execution. diff --git a/documentation/VS-Telemetry-Data.md b/documentation/VS-Telemetry-Data.md new file mode 100644 index 00000000000..5f7693bcbae --- /dev/null +++ b/documentation/VS-Telemetry-Data.md @@ -0,0 +1,138 @@ +# MSBuild Visual Studio Telemetry Data + +This document describes the telemetry data collected by MSBuild and sent to Visual Studio telemetry infrastructure. The telemetry helps the MSBuild team understand build patterns, identify issues, and improve the build experience. + +## Overview + +MSBuild collects telemetry at multiple levels: +1. **Build-level telemetry** - Overall build metrics and outcomes +2. **Task-level telemetry** - Information about task execution +3. **Target-level telemetry** - Information about target execution and incrementality +4. **Error categorization telemetry** - Classification of build failures + +All telemetry events use the `VS/MSBuild/` prefix as required by VS exporting/collection. Properties use the `VS.MSBuild.` prefix. + +--- + +## 1. Build Telemetry (`build` event) + +The primary telemetry event capturing overall build information. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `BuildDurationInMilliseconds` | double | Total build duration from start to finish | +| `InnerBuildDurationInMilliseconds` | double | Duration from when BuildManager starts (excludes server connection time) | +| `BuildEngineHost` | string | Host environment: "VS", "VSCode", "Azure DevOps", "GitHub Action", "CLI", etc. | +| `BuildSuccess` | bool | Whether the build succeeded | +| `BuildTarget` | string | The target(s) being built | +| `BuildEngineVersion` | Version | MSBuild engine version | +| `BuildEngineDisplayVersion` | string | Display-friendly engine version | +| `BuildEngineFrameworkName` | string | Runtime framework name | +| `BuildCheckEnabled` | bool | Whether BuildCheck (static analysis) was enabled | +| `MultiThreadedModeEnabled` | bool | Whether multi-threaded build mode was enabled | +| `SACEnabled` | bool | Whether Smart Application Control was enabled | +| `IsStandaloneExecution` | bool | True if MSBuild runs from command line | +| `InitialMSBuildServerState` | string | Server state before build: "cold", "hot", or null | +| `ServerFallbackReason` | string | If server was bypassed: "ServerBusy", "ConnectionError", or null | +| `ProjectPath` | string | Path to the project file being built | +| `FailureCategory` | string | Primary failure category when build fails (see Error Categorization) | +| `ErrorCounts` | object | Breakdown of errors by category (see Error Categorization) | + +--- + +## 2. Error Categorization + +When a build fails, errors are categorized to help identify the source of failures. + +### Error Categories + +| Category | Error Code Prefixes | Description | +|----------|---------------------|-------------| +| `Compiler` | CS, FS, VBC | C#, F#, and Visual Basic compiler errors | +| `MsBuildGeneral` | MSB4001-MSB4099, MSB4500-MSB4999 | General MSBuild errors | +| `MsBuildEvaluation` | MSB4100-MSB4199 | Project evaluation errors | +| `MsBuildExecution` | MSB4300-MSB4399, MSB5xxx-MSB6xxx | Build execution errors | +| `MsBuildGraph` | MSB4400-MSB4499 | Static graph build errors | +| `Task` | MSB3xxx | Task-related errors | +| `SdkResolvers` | MSB4200-MSB4299 | SDK resolution errors | +| `NetSdk` | NETSDK | .NET SDK errors | +| `NuGet` | NU | NuGet package errors | +| `BuildCheck` | BC | BuildCheck rule violations | +| `NativeToolchain` | LNK, C1xxx-C4xxx, CL | Native C/C++ toolchain errors (linker, compiler) | +| `CodeAnalysis` | CA, IDE | Code analysis and IDE analyzer errors | +| `Razor` | RZ | Razor compilation errors | +| `Wpf` | XC, MC | WPF/XAML compilation errors | +| `AspNet` | ASP, BL | ASP.NET and Blazor errors | +| `Other` | (all others) | Uncategorized errors | + +## 3. Task Telemetry + +### Task Factory Event (`build/tasks/taskfactory`) + +Tracks which task factories are being used. + +| Property | Type | Description | +|----------|------|-------------| +| `AssemblyTaskFactoryTasksExecutedCount` | int | Tasks loaded via AssemblyTaskFactory | +| `IntrinsicTaskFactoryTasksExecutedCount` | int | Built-in intrinsic tasks | +| `CodeTaskFactoryTasksExecutedCount` | int | Tasks created via CodeTaskFactory | +| `RoslynCodeTaskFactoryTasksExecutedCount` | int | Tasks created via RoslynCodeTaskFactory | +| `XamlTaskFactoryTasksExecutedCount` | int | Tasks created via XamlTaskFactory | +| `CustomTaskFactoryTasksExecutedCount` | int | Tasks from custom task factories | + +## 4. Build Incrementality Telemetry + +Classifies builds as full or incremental based on target execution patterns. + +### Incrementality Info (Activity Property) + +| Field | Type | Description | +|-------|------|-------------| +| `Classification` | enum | `Full`, `Incremental`, or `Unknown` | +| `TotalTargetsCount` | int | Total number of targets | +| `ExecutedTargetsCount` | int | Targets that ran | +| `SkippedTargetsCount` | int | Targets that were skipped | +| `SkippedDueToUpToDateCount` | int | Skipped because outputs were current | +| `SkippedDueToConditionCount` | int | Skipped due to false condition | +| `SkippedDueToPreviouslyBuiltCount` | int | Skipped because already built | +| `IncrementalityRatio` | double | Ratio of skipped to total (0.0-1.0) | + +A build is classified as **Incremental** when more than 70% of targets are skipped. + +--- + +## Privacy Considerations + +### Data Hashing + +Custom and potentially sensitive data is hashed using SHA-256 before being sent: +- **Custom task names** - Hashed to protect proprietary task names +- **Custom target names** - Hashed to protect proprietary target names +- **Custom task factory names** - Hashed if not in the known list +- **Metaproj target names** - Hashed to protect solution structure + +### Known Task Factory Names (Not Hashed) + +The following Microsoft-owned task factory names are sent in plain text: +- `AssemblyTaskFactory` +- `TaskHostFactory` +- `CodeTaskFactory` +- `RoslynCodeTaskFactory` +- `XamlTaskFactory` +- `IntrinsicTaskFactory` + +## Related Files + +| File | Description | +|------|-------------| +| [BuildTelemetry.cs](../src/Framework/Telemetry/BuildTelemetry.cs) | Main build telemetry class | +| [BuildInsights.cs](../src/Framework/Telemetry/BuildInsights.cs) | Container for detailed insights | +| [TelemetryDataUtils.cs](../src/Framework/Telemetry/TelemetryDataUtils.cs) | Data transformation utilities | +| [BuildErrorTelemetryTracker.cs](../src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs) | Error categorization | +| [ProjectTelemetry.cs](../src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs) | Per-project task telemetry | +| [LoggingConfigurationTelemetry.cs](../src/Framework/Telemetry/LoggingConfigurationTelemetry.cs) | Logger configuration | +| [BuildCheckTelemetry.cs](../src/Framework/Telemetry/BuildCheckTelemetry.cs) | BuildCheck telemetry | +| [KnownTelemetry.cs](../src/Framework/Telemetry/KnownTelemetry.cs) | Static telemetry accessors | +| [TelemetryConstants.cs](../src/Framework/Telemetry/TelemetryConstants.cs) | Telemetry naming constants | diff --git a/documentation/specs/VS-OpenTelemetry.md b/documentation/specs/VS-OpenTelemetry.md deleted file mode 100644 index 59d1f6e5d17..00000000000 --- a/documentation/specs/VS-OpenTelemetry.md +++ /dev/null @@ -1,198 +0,0 @@ -# Telemetry via OpenTelemetry design - -VS OTel provide packages compatible with ingesting data to their backend if we instrument it via OpenTelemetry traces (System.Diagnostics.Activity). -VS OTel packages are not open source so we need to conditionally include them in our build only for VS and MSBuild.exe - -> this formatting is a comment describing how the implementation turned out in 17.14 when our original goals were different - -[Onepager](https://github.com/dotnet/msbuild/blob/main/documentation/specs/proposed/telemetry-onepager.md) - -## Concepts - -It's a bit confusing how things are named in OpenTelemetry and .NET and VS Telemetry and what they do. - -| OTel concept | .NET/VS | Description | -| --- | --- | --- | -| Span/Trace | System.Diagnostics.Activity | Trace is a tree of Spans. Activities can be nested.| -| Tracer | System.Diagnostics.ActivitySource | Creates activites. | -| Processor/Exporter | VS OTel provided default config | filters and saves telemetry as files in a desired format | -| TracerProvider | OTel SDK TracerProvider | Singleton that is aware of processors, exporters and Tracers and listens (in .NET a bit looser relationship because it does not create Tracers just hooks to them) | -| Collector | VS OTel Collector | Sends to VS backend | - -## Requirements - -### Performance - -- If not sampled, no infra initialization overhead. -- Avoid allocations when not sampled. -- Has to have no impact on Core without opting into tracing, small impact on Framework -- No regression in VS perf ddrit scenarios. - -> there is an allocation regression when sampled, one of the reasons why it's not enabled by default - -### Privacy - -- Hashing data points that could identify customers (e.g. names of targets) -- Opt out capability - -### Security - -- Providing or/and documenting a method for creating a hook in Framework MSBuild -- If custom hooking solution will be used - document the security implications of hooking custom telemetry Exporters/Collectors in Framework -- other security requirements (transportation, rate limiting, sanitization, data access) are implemented by VS Telemetry library or the backend - -> hooking in Framework not implemented - -### Data handling - -- Implement head [Sampling](https://opentelemetry.io/docs/concepts/sampling/) with the granularity of a MSBuild.exe invocation/VS instance. -- VS Data handle tail sampling in their infrastructure not to overwhelm storage with a lot of build events. - -#### Data points - -The data sent via VS OpenTelemetry is neither a subset neither a superset of what is sent to SDK telemetry and it is not a purpose of this design to unify them. - -##### Basic info - -- Build duration -- Host -- Build success/failure -- Version -- Target (hashed) - -##### Evnironment - -- SAC (Smart app control) enabled - -##### Features - -- BuildCheck enabled -- Tasks runtimes and memory usage -- Tasks summary - whether they come from Nuget or are custom -- Targets summary - how many loaded and executed, how many come from nuget, how many come from metaproject - -The design should allow for easy instrumentation of additional data points. -> current implementation has only one datapoint and that is the whole build `vs/msbuild/build`, the instrumentaiton of additional datapoints is gated by first checking that telemetry is running and using `Activity` classes only in helper methods gated by `[MethodImpl(MethodImplOptions.NoInlining)]` to avoid System.Diagnostics.DiagnosticSource dll load. - -## Core `dotnet build` scenario - -- Telemetry should not be collected via VS OpenTelemetry mechanism because it's already collected in sdk. -- opt in to initialize the ActivitySource to avoid degrading performance. -- [baronfel/otel-startup-hook: A .NET CLR Startup Hook that exports OpenTelemetry metrics via the OTLP Exporter to an OpenTelemetry Collector](https://github.com/baronfel/otel-startup-hook/) and similar enable collecting telemetry data locally by listening to the ActivitySource prefix defined in MSBuild. - -> this hook can be used when the customer specifies that they want to listen to the prefix `Microsoft.VisualStudio.OpenTelemetry.MSBuild`, opt in by setting environment variables `MSBUILD_TELEMETRY_OPTIN=1`,`MSBUILD_TELEMETRY_SAMPLE_RATE=1.0` - -## Standalone MSBuild.exe scenario - -- Initialize and finalize in Xmake.cs - ActivitySource, TracerProvider, VS Collector -- overhead of starting VS collector is nonzero -- head sampling should avoid initializing if not sampled - -## VS in proc (devenv) scenario - -- VS can call `BuildManager` in a thread unsafe way the telemetry implementation has to be mindful of [BuildManager instances acquire its own BuildTelemetry instance by rokonec · Pull Request #8444 · dotnet/msbuild](https://github.com/dotnet/msbuild/pull/8444) - - ensure no race conditions in initialization - - only 1 TracerProvider with VS defined processing should exist -- Visual Studio should be responsible for having a running collector, we don't want this overhead in MSBuild and eventually many will use it - -> this was not achieved in 17.14 so we start collector every time - -## Implementation and MSBuild developer experience - -### ActivitySource names - -- Microsoft.VisualStudio.OpenTelemetry.MSBuild.Default - -### Sampling - -Our estimation from VS and SDK data is that there are 10M-100M build events per day. -For proportion estimation (of fairly common occurence in the builds), with not very strict confidnece (95%) and margin for error (5%) sampling 1:25000 would be enough. - -- this would apply for the DefaultActivitySource -- other ActivitySources could be sampled more frequently to get enough data -- Collecting has a cost, especially in standalone scenario where we have to start the collector. We might decide to undersample in standalone to avoid performance frequent impact. -- We want to avoid that cost when not sampled, therefore we prefer head sampling. -- Enables opt-in and opt-out for guaranteed sample or not sampled. -- nullable ActivitySource, using `?` when working with them, we can be initialized but not sampled -> it will not reinitialize but not collect telemetry. - -- for 17.14 we can't use the new OTel assemblies and their dependencies, so everything has to be opt in. -- eventually OpenTelemetry will be available and usable by default -- We can use experiments in VS to pass the environment variable to initialize - -> Targeted notification can be set that samples 100% of customers to which it is sent - -### Initialization at entrypoints - -- There are 2 entrypoints: - - for VS in BuildManager.BeginBuild - - for standalone in Xmake.cs Main - -### Exiting - -Force flush TracerProvider's exporter in BuildManager.EndBuild. -Dispose collector in Xmake.cs at the end of Main. - -### Configuration - -- Class that's responsible for configuring and initializing telemetry and handles optouts, holding tracer and collector. -- Wrapping source so that it has correct prefixes for VS backend to ingest. - -### Instrumenting - -2 ways of instrumenting: - -#### Instrument areas in code running in the main process - -```csharp -using (Activity? myActivity = OpenTelemetryManager.DefaultActivitySource?.StartActivity(TelemetryConstants.NameFromAConstantToAvoidAllocation)) -{ -// something happens here - -// add data to the trace -myActivity?.WithTag("SpecialEvent","fail") -} -``` - -Interface for classes holding telemetry data - -```csharp -IActivityTelemetryDataHolder data = new SomeData(); -... -myActivity?.WithTags(data); -``` - -> currently this should be gated in a separate method to avoid System.DiagnosticDiagnosticsource dll load. - -#### Default Build activity in EndBuild - -- this activity would always be created at the same point when sdk telemetry is sent in Core -- we can add data to it that we want in general builds -- the desired count of data from this should control the sample rate of DefaultActivitySource - -#### Multiple Activity Sources - -We want to create ActivitySources with different sample rates, this requires either implementation server side or a custom Processor. - -We potentially want apart from the Default ActivitySource: - -1. Other activity sources with different sample rates (in order to get significant data for rarer events such as custom tasks). -2. a way to override sampling decision - ad hoc starting telemetry infrastructure to catch rare events - -- Create a way of using a "HighPrioActivitySource" which would override sampling and initialize Collector in MSBuild.exe scenario/tracerprovider in VS. -- this would enable us to catch rare events - -> not implemented - -### Implementation details - -- `OpenTelemetryManager` - singleton that manages lifetime of OpenTelemetry objects listening to `Activity`ies, start by initializing in `Xmake` or `BuildManager`. -- Task and Target data is forwarded from worker nodes via `TelemetryForwarder` and `InternalTelemetryForwardingLogger` and then aggregated to stats and serialized in `TelemetryDataUtils` and attached to the default `vs/msbuild/build` event. - -## Future work when/if we decide to invest in telemetry again - -- avoid initializing/finalizing collector in VS when there is one running -- multiple levels of sampling for different types of events -- running by default with head sampling (simplifies instrumentation with `Activity`ies) -- implement anonymization consistently in an OTel processor and not ad hoc in each usage -- add datapoints helping perf optimization decisions/ reliability investigations diff --git a/documentation/specs/enable-binlog-collection-by-env-var.md b/documentation/specs/enable-binlog-collection-by-env-var.md new file mode 100644 index 00000000000..9db31a50f7b --- /dev/null +++ b/documentation/specs/enable-binlog-collection-by-env-var.md @@ -0,0 +1,158 @@ +# Enable Binary Log Collection via Environment Variable + +## Purpose + +Enable binary logging in CI/CD pipelines without modifying artifacts on disk. + +**Proposed solution:** An environment variable that enables diagnostic logging without touching any files on disk-no response file creation, no project file modifications, no build script changes. + +**Important for company-wide deployment:** When enabling this feature organization-wide (e.g., via CI/CD pipeline configuration), the team setting the environment variable may not be the team that owns individual codebases. Ensure stakeholders understand that builds with `/warnaserror` may be affected and be ready to mitigate this. + +### Demoting Warnings to Messages + +For scenarios where warnings would break builds (e.g., `/warnaserror` is enabled), set: + +```bash +set MSBUILD_LOGGING_ARGS_LEVEL=message +``` + +| Value | Behavior | +|-------|----------| +| `warning` (default) | Issues logged as warnings; may fail `/warnaserror` builds | +| `message` | Issues logged as low-importance messages; never fails builds | + +**Problem scenarios addressed:** + +- `-noAutoResponse` blocks response files entirely +- Creating `Directory.Build.rsp` requires writing new files to the source tree +- Modifying existing RSP files risks merge conflicts or unintended side effects +- Some build environments restrict write access to source directories + +### Why Not MSBUILDDEBUGENGINE? + +The existing `MSBUILDDEBUGENGINE=1` + `MSBUILDDEBUGPATH` mechanism works but has limitations for the desired CI/CD scenarios: + +- **Excessive logging:** Captures *all* MSBuild invocations including design-time builds, generating many files +- **No filename control:** Auto-generates filenames; cannot specify output path with `{}` placeholder for unique names +- **Debug overhead:** Enables additional debugging infrastructure beyond just binary logging + +## Supported Arguments + +- `-bl` / `/bl` / `-binarylogger` / `/binarylogger` (with optional parameters) +- `-check` / `/check` (with optional parameters) + +> **Note:** The `deferred` mode for `-check` is not currently supported. Enabling this feature requires changes to the MSBuild codebase. See section "Build Check (-check) Handling" below. + +> **Recommendation:** For CI/CD use, specify an **absolute path** with the `{}` placeholder (e.g., `-bl:C:\BuildLogs\build{}.binlog` or `-bl:/var/log/builds/build{}.binlog`) to generate unique filenames in a known location, avoiding CWD-relative paths that vary by build. + +**All other switches are blocked** to maintain diagnosability. + +### Rationale + +Environment variables that unexpectedly affect build behavior are notoriously difficult to diagnose (e.g., `Platform` is a known source of build issues). By restricting this environment variable to logging/diagnostic switches only, we ensure it cannot accidentally change build outcomes-only what gets recorded about the build. + +## Argument Processing Order + +1. **MSBuild.rsp** (next to MSBuild.exe) - skipped if `-noAutoResponse` present +2. **Directory.Build.rsp** (next to project) - skipped if `-noAutoResponse` present +3. **MSBUILD_LOGGING_ARGS** - always processed, regardless of `-noAutoResponse` +4. **Command-line arguments** + +### Why Precedence Doesn't Matter Here + +Since `MSBUILD_LOGGING_ARGS` only allows logging switches (`-bl` and `-check`), traditional precedence concerns don't apply: + +- **`-bl` is additive:** Each `-bl` argument creates a separate binlog file (requires [#12706](https://github.com/dotnet/msbuild/pull/12706)). Multiple sources specifying `-bl` simply result in multiple binlog files-there's no conflict to resolve. + +## Implementation Flow + +1. `MSBuildApp.Execute()` called +2. Check for `-noAutoResponse` in command line +3. Process response files (if no `-noAutoResponse`) +4. Read `MSBUILD_LOGGING_ARGS` environment variable +5. Validate and filter arguments +6. Prepend valid arguments to command line +7. Parse combined command line (merging happens here) +8. Execute build + +## Scope and Limitations + +### Supported Entry Points + +This environment variable only affects builds that go through MSBuild's `Main()` entry point: + +| Entry Point | Supported | Notes | +|-------------|-----------|-------| +| `MSBuild.exe` | ✅ Yes | | +| `dotnet build` | ✅ Yes | | +| `dotnet msbuild` | ✅ Yes | | +| Visual Studio (IDE builds) | ❌ No | Uses MSBuild API directly | +| `devenv.exe /build` | ❌ No | Uses MSBuild API directly | +| MSBuildWorkspace (Roslyn) | ❌ No | Uses MSBuild API directly | +| Custom build drivers via API | ❌ No | Any direct `Microsoft.Build` API usage | + +### API-Driven Builds + +For builds that use the MSBuild API directly (including Visual Studio and `devenv.exe /build`), this environment variable has no effect. + +**Alternative:** Use `MSBUILDDEBUGENGINE` to inject binlog collection into API-driven builds. This existing mechanism is already used for debugging Visual Studio builds and works across all MSBuild entry points. +```bash +# For API-driven builds (VS, devenv.exe /build, etc.) +set MSBUILDDEBUGENGINE=1 + +# For command-line builds (MSBuild.exe, dotnet build) +set MSBUILD_LOGGING_ARGS=-bl:build{}.binlog +``` + +## Warning Messages + +Issues are logged as **warnings** by default. Note that users with `/warnaserror` enabled will see these as errors-by opting into this environment variable, users also opt into these diagnostics. + +### Messages + +- **Informational:** "Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0}" - build continues with arguments applied +- **Unsupported argument:** "MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed." - the specific invalid argument is skipped, other valid arguments in the same env var are still processed (e.g., `-bl:a.binlog -maxcpucount:4` → `-bl:a.binlog` is applied, `-maxcpucount:4` is ignored with warning) +- **Malformed input:** "Error processing MSBUILD_LOGGING_ARGS environment variable: {0}" - the entire environment variable is skipped to avoid partial/unpredictable behavior, build proceeds as if the env var was not set + +## Build Check (-check) Handling + +### Deferred Analysis Mode + +`-check:deferred` enables binlog replay analysis with reduced build-time overhead: + +- **During build:** Flag recorded in binlog along with additional data needed for checks; BuildCheck NOT activated +- **During replay:** Binlog reader activates BuildCheck for analysis + +**Rationale:** BuildCheck analysis can be expensive and checks can fail the build. The environment variable is for diagnostics that can be analyzed later, allowing teams to record data with minimal impact to the build itself. + +### Example Workflow +```bash +# 1. Configure environment +set MSBUILD_LOGGING_ARGS=-bl:build{}.binlog -check:deferred + +# 2. Run build (reduced overhead, no BuildCheck analysis during build) +msbuild solution.sln + +# 3. Later: Replay binlog (BuildCheck analyzes recorded events) +msbuild build{}.binlog +``` + +## CI/CD Integration + + +### Environment Variable + +- Set `MSBUILD_LOGGING_ARGS=-bl:build{}.binlog` +- No file creation needed +- The `{}` placeholder generates unique filenames for each build invocation + +### Combining Both Approaches +```bash +# Environment provides base logging +set MSBUILD_LOGGING_ARGS=-bl:base{}.binlog -check:deferred + +# Command line adds specific logging +msbuild solution.sln -bl:detailed.binlog + +# Result: Two binlog files created (base{...}.binlog + detailed.binlog) +``` \ No newline at end of file diff --git a/documentation/specs/proposed/telemetry-onepager.md b/documentation/specs/proposed/telemetry-onepager.md deleted file mode 100644 index 5bc8f22f9ce..00000000000 --- a/documentation/specs/proposed/telemetry-onepager.md +++ /dev/null @@ -1,77 +0,0 @@ -# Telemetry - -We want to implement telemetry collection for VS/MSBuild.exe scenarios where we are currently not collecting data. VS OpenTelemetry initiative provides a good opportunity to use their infrastructure and library. -There is some data we collect via SDK which we want to make accessible. - -## Goals and Motivation - -We have limited data about usage of MSBuild by our customers in VS and no data about usage of standalone msbuild.exe. -This limits us in prioritization of features and scenarios to optimize performance for. -Over time we want to have comprehensive insight into how MSBuild is used in all scenarios. Collecting such a data without any constraints nor limitations would however be prohibitively expensive (from the data storage PoV and possibly as well from the client side performance impact PoV). Ability to sample / configure the collection is an important factor in deciding the instrumentation and collection tech stack. Implementing telemetry via VS OpenTelemetry initiative would give us this ability in the future. - -Goal: To have relevant data in that is actionable for decisions about development. Measuring real world performance impact of features (e.g. BuildCheck). Easily extensible telemetry infrastructure if we want to measure a new datapoint. - -## Impact -- Better planning of deployment of forces in MSBuild by product/team management. -- Customers can subscribe to telemetry locally to have data in standardized OpenTelemetry format - -## Stakeholders -- @Jan(Krivanek|Provaznik) design and implementation of telemetry via VS OTel. @ - using data we already have from SDK. -- @maridematte - documenting + dashboarding currently existing datapoints. -- MSBuild Team+Management – want insights from builds in VS -- VS OpenTelemetry team – provide support for VS OpenTelemetry collector library, want successful adoption -- SourceBuild – consulting and approving usage of OpenTelemetry -- MSBuild PM @baronfel – representing customers who want to monitor their builds locally - -### V1 Successful handover -- Shipped to Visual Studio -- Data queryable in Kusto -- Dashboards (even for pre-existing data - not introduced by this work) -- Customers are able to monitor with OpenTelemetry collector of choice (can be cut) - -## Risks -- Performance regression risks - it's another thing MSBuild would do and if the perf hit would be too bad it would need mitigation effort. -- It introduces a closed source dependency for VS and MSBuild.exe distribution methods which requires workarounds to remain compatible with SourceBuild policy (conditional compilation/build). -- Using a new VS API - might have gaps -- storage costs -- Potential additional costs and delays due to compliance with SourceBuild/VS data. - -## V1 Cost -5 months of .5 developer's effort ~ 50 dev days (dd) - -20-30dd JanPro OTel design + implementation, 10-15dd JanK design + implementation, 5-10dd Mariana/someone getting available data in order/"data science"/dashboards + external documentation - -Uncertainties: -It’s an exploratory project for VS OpenTelemetry, we'll be their first OSS component, so there might come up issues. SourceBuild compliance could introduce delays. - -## Plan -### V1 scope -- Collected data point definition -- Instrumented data points (as an example how the instrumentation and collection works) -- Telemetry sent to VS Telemetry in acceptable quantity -- Dashboards for collected data -- Hooking of customer's telemetry collection -- Documenting and leveraging pre-existing telemetry - -#### Out of scope -- Unifying telemetry for SDK MSBuild and MSBuild.exe/VS MSBuild. -- Thorough instrumentation of MSBuild -- Using MSBuild server -- Distributed tracing - -### Detailed cost -- Prototyping the libraries/mechanism for collecting telemetry data (month 1) 10dd - -- Defining usful data points (month 1) 5dd - -- Design and approval of hooking VSTelemetry collectors and OTel collectors (month 2) 10dd - -- Formalizing, agreeing to sourcebuild and other external requirements (month 2) 5dd - -- Instrumenting MSBuild with defined datapoints (month 3) 7dd - -- Creating dashboards/insights (month 4) 5dd - -- Documenting for customers how to hook their own telemetry collection (month 4) 3dd - -- Buffer for discovered issues (VSData Platform, SourceBuild, OpenTelemetry) and more investments (month 5) 5dd diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index c09ef8f9bf9..66693a74667 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -24,6 +24,9 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t ## Current Rotation of Change Waves +### 18.4 +- [Start throwing on null or empty paths in MultiProcess and MultiThreaded Task Environment Drivers.](https://github.com/dotnet/msbuild/pull/12914) + ### 18.3 - [Replace Transactional property with ChangeWave control, implement atomic file replacement with retry logic, and update tests.](https://github.com/dotnet/msbuild/pull/12627) diff --git a/documentation/wiki/CollectedTelemetry.md b/documentation/wiki/CollectedTelemetry.md index 0b897249788..aded23be6cd 100644 --- a/documentation/wiki/CollectedTelemetry.md +++ b/documentation/wiki/CollectedTelemetry.md @@ -100,6 +100,14 @@ Expressed and collected via [BuildTelemetry type](https://github.com/dotnet/msbu | >= 9.0.100 | Indication of enablement of BuildCheck feature. | | >= 9.0.100 | Indication of Smart App Control being in evaluation mode on machine executing the build. | | >= 10.0.100 | Indication if the build was run in multithreaded mode. | +| >= 10.0.200 | Primary failure category when BuildSuccess = false (one of: "Compiler", "MSBuildEngine", "Tasks", "SDK", "NuGet", "BuildCheck", "Other"). | +| >= 10.0.200 | Count of compiler errors encountered during the build. | +| >= 10.0.200 | Count of MSBuild engine errors encountered during the build. | +| >= 10.0.200 | Count of task errors encountered during the build. | +| >= 10.0.200 | Count of SDK errors encountered during the build. | +| >= 10.0.200 | Count of NuGet errors encountered during the build. | +| >= 10.0.200 | Count of BuildCheck errors encountered during the build. | +| >= 10.0.200 | Count of other errors encountered during the build. | ### Project Build diff --git a/documentation/wiki/MSBuild-Environment-Variables.md b/documentation/wiki/MSBuild-Environment-Variables.md index 76b73f435f4..859cc8e7f69 100644 --- a/documentation/wiki/MSBuild-Environment-Variables.md +++ b/documentation/wiki/MSBuild-Environment-Variables.md @@ -34,3 +34,5 @@ Some of the env variables listed here are unsupported, meaning there is no guara - Set this to force all tasks to run out of process (except inline tasks). - `MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC` - Set this to force all inline tasks to run out of process. It is not compatible with custom TaskFactories. +- `MSBUILD_CONSOLE_USE_DEFAULT_ENCODING` + - It opts out automatic console encoding UTF-8. Make Console use default encoding in the system. \ No newline at end of file diff --git a/eng/Signing.props b/eng/Signing.props index b2e4bff8ffe..00e8367eb86 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -13,9 +13,6 @@ - - - diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 5b13936e49b..433e4dba715 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,30 +6,30 @@ This file should be imported by eng/Versions.props - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 - 9.0.11 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 + 10.0.1 - 10.0.0-beta.25626.5 - 10.0.0-beta.25626.5 + 10.0.0-beta.26062.3 + 10.0.0-beta.26062.3 - 7.3.0-preview.1.50 + 7.4.0-rc.12 - 5.3.0-2.26051.1 + 5.4.0-2.26068.1 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1508f78d1ec..bcf567e3420 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,126 +1,126 @@ - + - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/arcade - d8dca0b41b903e7182e64543773390b969dab96b + 9f518f2be968c4c0102c2e3f8c793c5b7f28b731 - + https://github.com/nuget/nuget.client - a99b70cf718ff7842466a7eaeefa99b471cad517 + 367c40e337c0881682bc437b344511f482084d67 - + https://github.com/dotnet/roslyn - df7b5aaff073486376dad5d30b6d0ba45595d97d + a8d1f35c874c33cd877b4983c0a315b9437e77e3 - + https://github.com/dotnet/arcade - d8dca0b41b903e7182e64543773390b969dab96b + 9f518f2be968c4c0102c2e3f8c793c5b7f28b731 diff --git a/eng/Versions.props b/eng/Versions.props index 5ab5660455d..b13f1b3c952 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.3.0 + 18.4.0 preview 18.0.2 15.1.0.0 @@ -48,9 +48,10 @@ However, we can update, binding-redirect to, and distribute the newest version (that matches the VS-referenced versions) in order to get the benefits of updating. See uses of $(UseFrozenMaintenancePackageVersions) for details. --> - 4.6.0 - 6.1.0 - 4.6.0 + 4.6.3 + 6.1.2 + 4.6.3 + 4.6.1 @@ -59,8 +60,8 @@ - 0.2.104-beta - + 17.14.18 + @@ -80,7 +81,7 @@ - 10.0.100 + 10.0.101 diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 3437087c80f..b955fac6e13 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -80,7 +80,7 @@ jobs: # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 9423d71ca3a..b942a79ef02 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -293,11 +293,11 @@ stages: ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows ${{ else }}: name: NetCore1ESPool-Publishing-Internal - demands: ImageOverride -equals windows.vs2019.amd64 + demands: ImageOverride -equals windows.vs2022.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: diff --git a/eng/common/templates/variables/pool-providers.yml b/eng/common/templates/variables/pool-providers.yml index e0b19c14a07..18693ea120d 100644 --- a/eng/common/templates/variables/pool-providers.yml +++ b/eng/common/templates/variables/pool-providers.yml @@ -23,7 +23,7 @@ # # pool: # name: $(DncEngInternalBuildPool) -# demands: ImageOverride -equals windows.vs2019.amd64 +# demands: ImageOverride -equals windows.vs2022.amd64 variables: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - template: /eng/common/templates-official/variables/pool-providers.yml diff --git a/global.json b/global.json index 677476b7c8a..0d6a2066c27 100644 --- a/global.json +++ b/global.json @@ -14,6 +14,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25626.5" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26062.3" } } diff --git a/src/Build.OM.UnitTests/Construction/ConstructionEditing_Tests.cs b/src/Build.OM.UnitTests/Construction/ConstructionEditing_Tests.cs index aaa2c454210..81874f5afd2 100644 --- a/src/Build.OM.UnitTests/Construction/ConstructionEditing_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/ConstructionEditing_Tests.cs @@ -869,7 +869,7 @@ public static IEnumerable InsertMetadataElementAfterSiblingsTestData } [Theory] - [MemberData(nameof(InsertMetadataElementAfterSiblingsTestData))] + [MemberData(nameof(InsertMetadataElementAfterSiblingsTestData), DisableDiscoveryEnumeration = true)] public void InsertMetadataElementAfterSiblings(AddMetadata addMetadata, int position, string expectedItem) { Action act = (i, c, r) => { i.InsertAfterChild(c, r); }; @@ -911,7 +911,7 @@ public static IEnumerable InsertMetadataElementBeforeSiblingsTestData } [Theory] - [MemberData(nameof(InsertMetadataElementBeforeSiblingsTestData))] + [MemberData(nameof(InsertMetadataElementBeforeSiblingsTestData), DisableDiscoveryEnumeration = true)] public void InsertMetadataElementBeforeSiblings(AddMetadata addMetadata, int position, string expectedItem) { Action act = (i, c, r) => { i.InsertBeforeChild(c, r); }; diff --git a/src/Build.OM.UnitTests/Definition/DefinitionEditing_Tests.cs b/src/Build.OM.UnitTests/Definition/DefinitionEditing_Tests.cs index 6708096bd7e..2c046dd3f87 100644 --- a/src/Build.OM.UnitTests/Definition/DefinitionEditing_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/DefinitionEditing_Tests.cs @@ -1151,8 +1151,8 @@ public void RenameItem_StillMatchesWildcard() } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] - [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] + [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void RenameThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.Rename("foo"); }); @@ -1284,8 +1284,8 @@ public void ChangeItemTypeOnItemNeedingSplitting() } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] - [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] + [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void ChangeItemTypeThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.ItemType = "foo"; }); @@ -1446,16 +1446,16 @@ public void RemoveItem_IncludingFromIgnoringConditionList() } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] - [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] + [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void RemoveItemThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { p.RemoveItem(i); }); } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] - [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] + [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void RemoveItemsThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { p.RemoveItems(new[] { i }); }); @@ -1654,7 +1654,7 @@ public void RemoveMetadataAfterItemRemoved() } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void RemoveMetadataThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.RemoveMetadata("bar"); }, "bar"); @@ -1743,8 +1743,8 @@ public void SetMetadatumAfterRemoved3() } [Theory] - [MemberData(nameof(ItemElementsThatRequireSplitting))] - [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))] + [MemberData(nameof(ItemElementsThatRequireSplitting), DisableDiscoveryEnumeration = true)] + [MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting), DisableDiscoveryEnumeration = true)] public void SetMetadataThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject) { AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.SetMetadataValue("foo", "bar"); }); diff --git a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs index 75b2e82319d..600f24ad21e 100644 --- a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs @@ -3625,7 +3625,7 @@ public static IEnumerable UpdateAndRemoveShouldWorkWithEscapedCharacte } [Theory] - [MemberData(nameof(UpdateAndRemoveShouldWorkWithEscapedCharactersTestData))] + [MemberData(nameof(UpdateAndRemoveShouldWorkWithEscapedCharactersTestData), DisableDiscoveryEnumeration = true)] public void UpdateAndRemoveShouldWorkWithEscapedCharacters(string projectContents, string include, string update, string remove, string[] expectedInclude, Dictionary[] expectedMetadata) { var formattedProjectContents = string.Format(projectContents, include, update, remove); diff --git a/src/Build.OM.UnitTests/Definition/Project_Tests.cs b/src/Build.OM.UnitTests/Definition/Project_Tests.cs index ca0fc1cde72..d1e43441816 100644 --- a/src/Build.OM.UnitTests/Definition/Project_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/Project_Tests.cs @@ -3065,7 +3065,7 @@ public static IEnumerable GetItemProvenanceByProjectItemTestData } [Theory] - [MemberData(nameof(GetItemProvenanceByProjectItemTestData))] + [MemberData(nameof(GetItemProvenanceByProjectItemTestData), DisableDiscoveryEnumeration = true)] public void GetItemProvenanceByProjectItem(string items, string itemValue, int itemPosition, ProvenanceResultTupleList expected) { var formattedProject = string.Format(ProjectWithItemGroup, items); @@ -3486,7 +3486,7 @@ public static IEnumerable GetItemProvenanceShouldWorkWithEscapedCharac } } [Theory] - [MemberData(nameof(GetItemProvenanceShouldWorkWithEscapedCharactersTestData))] + [MemberData(nameof(GetItemProvenanceShouldWorkWithEscapedCharactersTestData), DisableDiscoveryEnumeration = true)] public void GetItemProvenanceShouldWorkWithEscapedCharacters(string project, string provenanceArgument, ProvenanceResultTupleList expectedProvenance) { AssertProvenanceResult(expectedProvenance, project, provenanceArgument); diff --git a/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs b/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs index 500f4ded77f..82e190d1e1d 100644 --- a/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs @@ -244,7 +244,15 @@ public void VerifyGoodTaskInstantiation() ITask createdTask = null; try { - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -276,7 +284,15 @@ public void VerifyMatchingTaskParametersDontLaunchTaskHost1() { TaskHostParameters taskParameters = new (XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildArchitectureValues.any); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -308,7 +324,15 @@ public void VerifyMatchingTaskParametersDontLaunchTaskHost2() { TaskHostParameters taskParameters = new (XMakeAttributes.GetCurrentMSBuildRuntime(), XMakeAttributes.GetCurrentMSBuildArchitecture()); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -342,7 +366,15 @@ public void VerifyMatchingUsingTaskParametersDontLaunchTaskHost1() SetupTaskFactory(taskParameters, false /* don't want task host */); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -376,7 +408,15 @@ public void VerifyMatchingUsingTaskParametersDontLaunchTaskHost2() SetupTaskFactory(taskParameters, false /* don't want task host */); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -412,7 +452,15 @@ public void VerifyMatchingParametersDontLaunchTaskHost() TaskHostParameters taskParameters = new (architecture: XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -446,7 +494,15 @@ public void VerifyNonmatchingUsingTaskParametersLaunchTaskHost() SetupTaskFactory(taskParameters, false /* don't want task host */); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -478,7 +534,15 @@ public void VerifyNonmatchingTaskParametersLaunchTaskHost() { TaskHostParameters taskParameters = new(XMakeAttributes.MSBuildRuntimeValues.clr2, XMakeAttributes.MSBuildArchitectureValues.any); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -514,7 +578,15 @@ public void VerifyNonmatchingParametersLaunchTaskHost() TaskHostParameters taskParameters = new(architecture: XMakeAttributes.MSBuildArchitectureValues.any); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -546,7 +618,15 @@ public void VerifyExplicitlyLaunchTaskHost() { SetupTaskFactory(TaskHostParameters.Empty, true /* want task host */, true); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -580,7 +660,15 @@ public void VerifyExplicitlyLaunchTaskHostEvenIfParametersMatch1() SetupTaskFactory(taskParameters, true /* want task host */, isTaskHostFactory: true); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -614,7 +702,15 @@ public void VerifyExplicitlyLaunchTaskHostEvenIfParametersMatch2() TaskHostParameters taskParameters = new (XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildArchitectureValues.any); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -649,7 +745,15 @@ public void VerifySameFactoryCanGenerateDifferentTaskInstances() try { // #1: don't launch task host - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), TaskHostParameters.Empty, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + TaskHostParameters.Empty, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif @@ -673,7 +777,15 @@ public void VerifySameFactoryCanGenerateDifferentTaskInstances() // #2: launch task host TaskHostParameters taskParameters = new(XMakeAttributes.MSBuildRuntimeValues.clr2, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); - createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, + createdTask = _taskFactory.CreateTaskInstance( + ElementLocation.Create("MSBUILD"), + null, + new MockHost(), + taskParameters, + projectFile: "proj.proj", +#if !NET35 + hostServices: null, +#endif #if FEATURE_APPDOMAIN new AppDomainSetup(), #endif diff --git a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs index 06cf011f5a2..f9e0cb80237 100644 --- a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs @@ -1513,12 +1513,14 @@ public void CancelledBuildWithUnexecutedSubmission() } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously: needs to be async for xunit's timeout system +#pragma warning disable IDE0390 // Method can be made synchronous /// /// A canceled build /// [Fact(Timeout = 20_000)] public async System.Threading.Tasks.Task CancelledBuild() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning restore IDE0390 // Method can be made synchronous { Console.WriteLine("Starting CancelledBuild test that is known to hang."); string contents = CleanupFileContents(@" @@ -1801,7 +1803,6 @@ public void OverlappingBuildsOfTheSameProjectDifferentTargetsAreAllowed() "); - Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true); ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); _buildManager.BeginBuild(_parameters); diff --git a/src/Build.UnitTests/BackEnd/BuildTelemetryErrorCategorization_Tests.cs b/src/Build.UnitTests/BackEnd/BuildTelemetryErrorCategorization_Tests.cs new file mode 100644 index 00000000000..19e28f562b1 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/BuildTelemetryErrorCategorization_Tests.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Telemetry; +using Microsoft.Build.Shared; +using Shouldly; +using Xunit; +using static Microsoft.Build.BackEnd.Logging.BuildErrorTelemetryTracker; + +#nullable disable + +namespace Microsoft.Build.UnitTests.BackEnd; + +public class BuildTelemetryErrorCategorization_Tests +{ + [Theory] + [InlineData("CS0103", null, nameof(ErrorCategory.Compiler))] + [InlineData("CS1002", "CS", nameof(ErrorCategory.Compiler))] + [InlineData("VBC30451", "VBC", nameof(ErrorCategory.Compiler))] + [InlineData("FS0039", null, nameof(ErrorCategory.Compiler))] + [InlineData("MSB4018", null, nameof(ErrorCategory.MSBuildGeneral))] + [InlineData("MSB4236", null, nameof(ErrorCategory.SDKResolvers))] + [InlineData("MSB3026", null, nameof(ErrorCategory.Tasks))] + [InlineData("NETSDK1045", null, nameof(ErrorCategory.NETSDK))] + [InlineData("NU1101", null, nameof(ErrorCategory.NuGet))] + [InlineData("BC0001", null, nameof(ErrorCategory.BuildCheck))] + [InlineData("CUSTOM001", null, nameof(ErrorCategory.Other))] + [InlineData(null, null, nameof(ErrorCategory.Other))] + [InlineData("", null, nameof(ErrorCategory.Other))] + public void ErrorCategorizationWorksCorrectly(string errorCode, string subcategory, string expectedCategory) + { + // Create a LoggingService + var loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.OnlyLogCriticalEvents = false; + + try + { + // Log an error with the specified code + var errorEvent = new BuildErrorEventArgs( + subcategory, + errorCode, + "file.cs", + 1, + 1, + 0, + 0, + "Test error message", + "helpKeyword", + "sender"); + + loggingService.LogBuildEvent(errorEvent); + + // Populate telemetry + var buildTelemetry = new BuildTelemetry(); + loggingService.PopulateBuildTelemetryWithErrors(buildTelemetry); + + // Verify the category is set correctly + buildTelemetry.FailureCategory.ShouldBe(expectedCategory); + + // Verify the appropriate count is incremented + switch (expectedCategory) + { + case nameof(ErrorCategory.Compiler): + buildTelemetry.ErrorCounts.Compiler.ShouldBe(1); + break; + case nameof(ErrorCategory.MSBuildGeneral): + buildTelemetry.ErrorCounts.MsBuildGeneral.ShouldBe(1); + break; + case nameof(ErrorCategory.Tasks): + buildTelemetry.ErrorCounts.Task.ShouldBe(1); + break; + case nameof(ErrorCategory.SDKResolvers): + buildTelemetry.ErrorCounts.SdkResolvers.ShouldBe(1); + break; + case nameof(ErrorCategory.NETSDK): + buildTelemetry.ErrorCounts.NetSdk.ShouldBe(1); + break; + case nameof(ErrorCategory.NuGet): + buildTelemetry.ErrorCounts.NuGet.ShouldBe(1); + break; + case nameof(ErrorCategory.BuildCheck): + buildTelemetry.ErrorCounts.BuildCheck.ShouldBe(1); + break; + case nameof(ErrorCategory.Other): + buildTelemetry.ErrorCounts.Other.ShouldBe(1); + break; + } + } + finally + { + loggingService.ShutdownComponent(); + } + } + + [Fact] + public void MultipleErrorsAreCountedByCategory() + { + var loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.OnlyLogCriticalEvents = false; + + try + { + // Log multiple errors of different categories + var errors = new[] + { + new BuildErrorEventArgs(null, "CS0103", "file.cs", 1, 1, 0, 0, "Error 1", null, "sender"), + new BuildErrorEventArgs(null, "CS1002", "file.cs", 2, 1, 0, 0, "Error 2", null, "sender"), + new BuildErrorEventArgs(null, "MSB4018", "file.proj", 10, 5, 0, 0, "Error 3", null, "sender"), + new BuildErrorEventArgs(null, "MSB3026", "file.proj", 15, 3, 0, 0, "Error 4", null, "sender"), + new BuildErrorEventArgs(null, "NU1101", "file.proj", 20, 1, 0, 0, "Error 5", null, "sender"), + new BuildErrorEventArgs(null, "CUSTOM001", "file.txt", 1, 1, 0, 0, "Error 6", null, "sender"), + }; + + foreach (var error in errors) + { + loggingService.LogBuildEvent(error); + } + + // Populate telemetry + var buildTelemetry = new BuildTelemetry(); + loggingService.PopulateBuildTelemetryWithErrors(buildTelemetry); + + // Verify counts + buildTelemetry.ErrorCounts.Compiler.ShouldBe(2); + buildTelemetry.ErrorCounts.MsBuildGeneral.ShouldBe(1); + buildTelemetry.ErrorCounts.Task.ShouldBe(1); + buildTelemetry.ErrorCounts.NuGet.ShouldBe(1); + buildTelemetry.ErrorCounts.Other.ShouldBe(1); + + // Primary category should be Compiler (highest count) + buildTelemetry.FailureCategory.ShouldBe(nameof(ErrorCategory.Compiler)); + } + finally + { + loggingService.ShutdownComponent(); + } + } + + [Fact] + public void PrimaryCategoryIsSetToHighestErrorCount() + { + var loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.OnlyLogCriticalEvents = false; + + try + { + // Log errors with Tasks having the highest count + var errors = new[] + { + new BuildErrorEventArgs(null, "MSB3026", "file.proj", 1, 1, 0, 0, "Task Error 1", null, "sender"), + new BuildErrorEventArgs(null, "MSB3027", "file.proj", 2, 1, 0, 0, "Task Error 2", null, "sender"), + new BuildErrorEventArgs(null, "MSB3028", "file.proj", 3, 1, 0, 0, "Task Error 3", null, "sender"), + new BuildErrorEventArgs(null, "CS0103", "file.cs", 4, 1, 0, 0, "Compiler Error", null, "sender"), + }; + + foreach (var error in errors) + { + loggingService.LogBuildEvent(error); + } + + // Populate telemetry + var buildTelemetry = new BuildTelemetry(); + loggingService.PopulateBuildTelemetryWithErrors(buildTelemetry); + + // Primary category should be Tasks (3 errors vs 1 compiler error) + buildTelemetry.FailureCategory.ShouldBe(nameof(ErrorCategory.Tasks)); + buildTelemetry.ErrorCounts.Task.ShouldBe(3); + buildTelemetry.ErrorCounts.Compiler.ShouldBe(1); + } + finally + { + loggingService.ShutdownComponent(); + } + } + + [Fact] + public void SubcategoryIsUsedForCompilerErrors() + { + var loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.OnlyLogCriticalEvents = false; + + try + { + // Log an error with subcategory "CS" (common for C# compiler errors) + var errorEvent = new BuildErrorEventArgs( + "CS", // subcategory + "CS0103", + "file.cs", + 1, + 1, + 0, + 0, + "The name 'foo' does not exist in the current context", + "helpKeyword", + "csc"); + + loggingService.LogBuildEvent(errorEvent); + + // Populate telemetry + var buildTelemetry = new BuildTelemetry(); + loggingService.PopulateBuildTelemetryWithErrors(buildTelemetry); + + // Should be categorized as Compiler based on subcategory + buildTelemetry.FailureCategory.ShouldBe(nameof(ErrorCategory.Compiler)); + buildTelemetry.ErrorCounts.Compiler.ShouldBe(1); + } + finally + { + loggingService.ShutdownComponent(); + } + } +} diff --git a/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs index 954832fce12..41afe33d841 100644 --- a/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs @@ -7,6 +7,8 @@ using Microsoft.Build.Framework.Telemetry; using Shouldly; using Xunit; +using static Microsoft.Build.BackEnd.Logging.BuildErrorTelemetryTracker; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; namespace Microsoft.Build.UnitTests.Telemetry; @@ -123,4 +125,83 @@ public void BuildTelemetryHandleNullsInRecordedTimes() buildTelemetry.FinishedAt = DateTime.MaxValue; buildTelemetry.GetProperties().ShouldBeEmpty(); } + + [Fact] + public void BuildTelemetryIncludesFailureCategoryProperties() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + + buildTelemetry.BuildSuccess = false; + buildTelemetry.FailureCategory = nameof(ErrorCategory.Compiler); + buildTelemetry.ErrorCounts = new ErrorCountsInfo( + Compiler: 5, + MsBuildGeneral: 2, + MsBuildEvaluation: null, + MsBuildExecution: null, + MsBuildGraph: null, + Task: 1, + SdkResolvers: null, + NetSdk: null, + NuGet: 3, + BuildCheck: null, + NativeToolchain: null, + CodeAnalysis: null, + Razor: null, + Wpf: null, + AspNet: null, + Other: 1); + + var properties = buildTelemetry.GetProperties(); + + properties["BuildSuccess"].ShouldBe("False"); + properties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Compiler)); + properties.ContainsKey("ErrorCounts").ShouldBeTrue(); + + var activityProperties = buildTelemetry.GetActivityProperties(); + activityProperties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Compiler)); + var errorCounts = activityProperties["ErrorCounts"] as ErrorCountsInfo; + errorCounts.ShouldNotBeNull(); + errorCounts.Compiler.ShouldBe(5); + errorCounts.MsBuildGeneral.ShouldBe(2); + errorCounts.Task.ShouldBe(1); + errorCounts.NuGet.ShouldBe(3); + errorCounts.Other.ShouldBe(1); + errorCounts.SdkResolvers.ShouldBeNull(); + errorCounts.NetSdk.ShouldBeNull(); + errorCounts.BuildCheck.ShouldBeNull(); + } + + [Fact] + public void BuildTelemetryActivityPropertiesIncludesFailureData() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + + buildTelemetry.BuildSuccess = false; + buildTelemetry.FailureCategory = nameof(ErrorCategory.Tasks); + buildTelemetry.ErrorCounts = new ErrorCountsInfo( + Compiler: null, + MsBuildGeneral: null, + MsBuildEvaluation: null, + MsBuildExecution: null, + MsBuildGraph: null, + Task: 10, + SdkResolvers: null, + NetSdk: null, + NuGet: null, + BuildCheck: null, + NativeToolchain: null, + CodeAnalysis: null, + Razor: null, + Wpf: null, + AspNet: null, + Other: null); + + var activityProperties = buildTelemetry.GetActivityProperties(); + + activityProperties["BuildSuccess"].ShouldBe(false); + activityProperties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Tasks)); + var errorCounts = activityProperties["ErrorCounts"] as ErrorCountsInfo; + errorCounts.ShouldNotBeNull(); + errorCounts.Task.ShouldBe(10); + } } diff --git a/src/Build.UnitTests/BackEnd/MockLoggingService.cs b/src/Build.UnitTests/BackEnd/MockLoggingService.cs index fa5f97176c2..10c40c8159a 100644 --- a/src/Build.UnitTests/BackEnd/MockLoggingService.cs +++ b/src/Build.UnitTests/BackEnd/MockLoggingService.cs @@ -677,6 +677,11 @@ public bool HasBuildSubmissionLoggedErrors(int submissionId) return false; } + public void PopulateBuildTelemetryWithErrors(Framework.Telemetry.BuildTelemetry buildTelemetry) + { + // Mock implementation does nothing + } + public ICollection GetWarningsAsErrors(BuildEventContext context) { throw new NotImplementedException(); diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 51d20a9b041..b56a59aa4fd 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -23,12 +23,12 @@ public class ProjectTelemetry_Tests public void TrackTaskSubclassing_TracksSealedTasks() { var telemetry = new ProjectTelemetry(); - + // Sealed task should be tracked if it derives from Microsoft task telemetry.TrackTaskSubclassing(typeof(TestSealedTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should track sealed tasks that inherit from Microsoft tasks properties.Count.ShouldBe(1); properties.ShouldContainKey("Microsoft_Build_Utilities_Task"); @@ -42,12 +42,12 @@ public void TrackTaskSubclassing_TracksSealedTasks() public void TrackTaskSubclassing_TracksSubclass() { var telemetry = new ProjectTelemetry(); - + // User task inheriting from Microsoft.Build.Utilities.Task telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should track the Microsoft.Build.Utilities.Task base class properties.Count.ShouldBe(1); properties.ShouldContainKey("Microsoft_Build_Utilities_Task"); @@ -61,12 +61,12 @@ public void TrackTaskSubclassing_TracksSubclass() public void TrackTaskSubclassing_IgnoresMicrosoftOwnedTasks() { var telemetry = new ProjectTelemetry(); - + // Microsoft-owned task should not be tracked even if non-sealed telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: true); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should not track Microsoft-owned tasks properties.Count.ShouldBe(0); } @@ -78,13 +78,13 @@ public void TrackTaskSubclassing_IgnoresMicrosoftOwnedTasks() public void TrackTaskSubclassing_TracksMultipleSubclasses() { var telemetry = new ProjectTelemetry(); - + // Track multiple user tasks telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: false); telemetry.TrackTaskSubclassing(typeof(AnotherUserTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should aggregate counts for the same base class properties.Count.ShouldBe(1); properties["Microsoft_Build_Utilities_Task"].ShouldBe("2"); @@ -97,13 +97,13 @@ public void TrackTaskSubclassing_TracksMultipleSubclasses() public void TrackTaskSubclassing_HandlesNull() { var telemetry = new ProjectTelemetry(); - + #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type telemetry.TrackTaskSubclassing(null, isMicrosoftOwned: false); #pragma warning restore CS8625 - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + properties.Count.ShouldBe(0); } @@ -112,7 +112,7 @@ public void TrackTaskSubclassing_HandlesNull() /// private System.Collections.Generic.Dictionary GetMSBuildTaskSubclassProperties(ProjectTelemetry telemetry) { - var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", + var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; } @@ -169,7 +169,7 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() var events = new System.Collections.Generic.List(); var logger = new Microsoft.Build.Logging.ConsoleLogger(LoggerVerbosity.Diagnostic); - + using var projectCollection = new ProjectCollection(); using var stringReader = new System.IO.StringReader(projectContent); using var xmlReader = System.Xml.XmlReader.Create(stringReader); @@ -177,7 +177,7 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() // Build the project var result = project.Build(); - + result.ShouldBeTrue(); } } diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index 0a03b507a4c..acb1bb6e832 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; +using Microsoft.Build.Shared; using Shouldly; using Xunit; @@ -358,5 +359,79 @@ public void TaskEnvironment_MultithreadedEnvironment_ShouldBeIsolatedFromSystem( Environment.SetEnvironmentVariable(testVarName, null); } } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(string environmentType) + { + // Construct a path containing an invalid path character + char invalidChar = Path.GetInvalidPathChars().FirstOrDefault(); + string invalidPath = "invalid" + invalidChar + "path"; + + var taskEnvironment = CreateTaskEnvironment(environmentType); + + try + { + // Should not throw on invalid path characters + var absolutePath = taskEnvironment.GetAbsolutePath(invalidPath); + + // The result should contain the invalid path combined with the base directory + absolutePath.Value.ShouldNotBeNullOrEmpty(); + absolutePath.Value.ShouldContain(invalidPath); + } + finally + { + DisposeTaskEnvironment(taskEnvironment); + } + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_ReturnsProjectDirectory(string environmentType) + { + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // Empty path should absolutize to project directory (Path.Combine behavior) + var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty); + + absolutePath.Value.ShouldBe(taskEnvironment.ProjectDirectory.Value); + absolutePath.OriginalValue.ShouldBe(string.Empty); + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath(string environmentType) + { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // When Wave18_4 is disabled, null path returns as-is + var absolutePath = taskEnvironment.GetAbsolutePath(null!); + + absolutePath.Value.ShouldBeNull(); + absolutePath.OriginalValue.ShouldBeNull(); + + ChangeWaves.ResetStateForTests(); + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Enabled_Throws(string environmentType) + { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // When Wave18_4 is enabled, null path should throw + Should.Throw(() => taskEnvironment.GetAbsolutePath(null!)); + + ChangeWaves.ResetStateForTests(); + } } } diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs index 6db852567a5..8be60f5b825 100644 --- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs @@ -992,8 +992,12 @@ public void TestTaskResolutionFailureWithUsingTask() ElementLocation.Create("none", 1, 1), this, false, + projectFile: "proj.proj", #if FEATURE_APPDOMAIN null, +#endif +#if !NET35 + null, #endif false, CancellationToken.None, @@ -1021,8 +1025,12 @@ public void TestTaskResolutionFailureWithNoUsingTask() ElementLocation.Create("none", 1, 1), this, false, + projectFile: "proj.proj", #if FEATURE_APPDOMAIN null, +#endif +#if !NET35 + null, #endif false, CancellationToken.None, @@ -1265,8 +1273,12 @@ private void InitializeHost(bool throwOnExecute) ElementLocation.Create("none", 1, 1), this, false, + projectFile: "proj.proj", #if FEATURE_APPDOMAIN null, +#endif +#if !NET35 + null, #endif false, CancellationToken.None, diff --git a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs index 959142d4a67..da40bef3702 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs @@ -43,6 +43,9 @@ public void ConstructorWithNullName() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -53,6 +56,8 @@ public void ConstructorWithNullName() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: null, taskLocation: @"c:\my tasks\mytask.dll", @@ -78,6 +83,9 @@ public void ConstructorWithEmptyName() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -88,6 +96,8 @@ public void ConstructorWithEmptyName() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: String.Empty, taskLocation: @"c:\my tasks\mytask.dll", @@ -113,6 +123,9 @@ public void ConstructorWithNullLocation() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -123,6 +136,8 @@ public void ConstructorWithNullLocation() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: null, @@ -150,6 +165,9 @@ public void ConstructorWithEmptyLocation() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -160,6 +178,8 @@ public void ConstructorWithEmptyLocation() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: String.Empty, @@ -185,6 +205,9 @@ public void TestValidConstructors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -195,6 +218,8 @@ public void TestValidConstructors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -211,6 +236,9 @@ public void TestValidConstructors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -221,6 +249,8 @@ public void TestValidConstructors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -238,6 +268,9 @@ public void TestValidConstructors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -248,6 +281,8 @@ public void TestValidConstructors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -270,6 +305,9 @@ public void TestValidConstructors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -280,6 +318,8 @@ public void TestValidConstructors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -302,6 +342,9 @@ public void TestValidConstructors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -312,6 +355,8 @@ public void TestValidConstructors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -341,6 +386,9 @@ public void TestTranslationWithNullDictionary() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -351,6 +399,8 @@ public void TestTranslationWithNullDictionary() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -392,10 +442,15 @@ public void TestTranslationWithAppDomainSetup(byte[] configBytes) buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif appDomainSetup: setup, lineNumberOfTask: 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -438,6 +493,9 @@ public void TestTranslationWithEmptyDictionary() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -448,6 +506,8 @@ public void TestTranslationWithEmptyDictionary() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -489,6 +549,9 @@ public void TestTranslationWithValueTypesInDictionary() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -499,6 +562,8 @@ public void TestTranslationWithValueTypesInDictionary() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -538,6 +603,9 @@ public void TestTranslationWithITaskItemInDictionary() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -548,6 +616,8 @@ public void TestTranslationWithITaskItemInDictionary() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -586,6 +656,9 @@ public void TestTranslationWithITaskItemArrayInDictionary() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -596,6 +669,8 @@ public void TestTranslationWithITaskItemArrayInDictionary() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -641,6 +716,9 @@ public void TestTranslationWithWarningsAsErrors() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -651,6 +729,8 @@ public void TestTranslationWithWarningsAsErrors() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", @@ -691,6 +771,9 @@ public void TestTranslationWithWarningsAsMessages() buildProcessEnvironment: null, culture: Thread.CurrentThread.CurrentCulture, uiCulture: Thread.CurrentThread.CurrentUICulture, +#if !NET35 + null, +#endif #if FEATURE_APPDOMAIN appDomainSetup: #if FEATURE_APPDOMAIN @@ -701,6 +784,8 @@ public void TestTranslationWithWarningsAsMessages() 1, columnNumberOfTask: 1, projectFileOfTask: @"c:\my project\myproj.proj", + targetName: "TargetName", + projectFile: "proj.proj", continueOnError: _continueOnErrorDefault, taskName: "TaskName", taskLocation: @"c:\MyTasks\MyTask.dll", diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index 7156a81eb58..55884569139 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -748,6 +748,182 @@ public void ParseParameters_InvalidParameter_ThrowsLoggerException(string parame File.Create(_logFile).Dispose(); } + #region ExtractFilePathFromParameters Tests + + [Theory] + [InlineData(null, "msbuild.binlog")] + [InlineData("", "msbuild.binlog")] + [InlineData("output.binlog", "output.binlog")] + [InlineData("LogFile=output.binlog", "output.binlog")] + [InlineData("output.binlog;ProjectImports=None", "output.binlog")] + [InlineData("ProjectImports=None;output.binlog", "output.binlog")] + [InlineData("ProjectImports=None;LogFile=output.binlog;OmitInitialInfo", "output.binlog")] + [InlineData("ProjectImports=None", "msbuild.binlog")] // No path specified + [InlineData("OmitInitialInfo", "msbuild.binlog")] // No path specified + public void ExtractFilePathFromParameters_ReturnsExpectedPath(string parameters, string expectedFileName) + { + string result = BinaryLogger.ExtractFilePathFromParameters(parameters); + + // The method returns full paths, so check just the filename + Path.GetFileName(result).ShouldBe(expectedFileName); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ExtractFilePathFromParameters_ReturnsFullPath() + { + string result = BinaryLogger.ExtractFilePathFromParameters("mylog.binlog"); + + // Should be a full path, not relative + Path.IsPathRooted(result).ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ExtractNonPathParameters Tests + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("output.binlog", "")] // Path only, no config + [InlineData("LogFile=output.binlog", "")] // Path only, no config + [InlineData("ProjectImports=None", "ProjectImports=None")] + [InlineData("OmitInitialInfo", "OmitInitialInfo")] + [InlineData("output.binlog;ProjectImports=None", "ProjectImports=None")] + [InlineData("output.binlog;ProjectImports=None;OmitInitialInfo", "OmitInitialInfo;ProjectImports=None")] // Sorted + [InlineData("OmitInitialInfo;output.binlog;ProjectImports=None", "OmitInitialInfo;ProjectImports=None")] // Sorted + public void ExtractNonPathParameters_ReturnsExpectedConfig(string parameters, string expectedConfig) + { + string result = BinaryLogger.ExtractNonPathParameters(parameters); + result.ShouldBe(expectedConfig); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ProcessParameters Tests + + [Fact] + public void ProcessParameters_NullArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(null); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_EmptyArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(Array.Empty()); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_SingleParameter_ReturnsOneParameterSet() + { + var result = BinaryLogger.ProcessParameters(new[] { "output.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(1); + result.DistinctParameterSets[0].ShouldBe("output.binlog"); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MultipleIdenticalConfigs_OptimizesWithAdditionalPaths() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog", "3.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(3); + result.AllConfigurationsIdentical.ShouldBeTrue(); + result.AdditionalFilePaths.Count.ShouldBe(2); // 2.binlog and 3.binlog + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DifferentConfigs_NoOptimization() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog;ProjectImports=None" }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_FilteredOut() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "1.binlog", "2.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // 1.binlog and 2.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate of 1.binlog + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_CaseInsensitive() + { + var result = BinaryLogger.ProcessParameters(new[] { "Output.binlog", "output.BINLOG", "other.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // Output.binlog and other.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MixedConfigsWithDuplicates_HandledCorrectly() + { + var result = BinaryLogger.ProcessParameters(new[] { + "1.binlog", + "2.binlog;ProjectImports=None", + "1.binlog;ProjectImports=None" // Different config but same path - filtered as duplicate + }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.DuplicateFilePaths.Count.ShouldBe(1); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + public void Dispose() { _env.Dispose(); diff --git a/src/Build.UnitTests/Construction/SolutionFile_OldParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_OldParser_Tests.cs index 7688d60acb9..0d197044b63 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_OldParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_OldParser_Tests.cs @@ -2555,7 +2555,7 @@ public void AbsolutePathShouldHandleUriLikeRelativePathsOnUnix() /// /// Test to verify that the fix for issue #1769 works by directly testing - /// FileUtilities.FixFilePath integration in AbsolutePath. + /// FrameworkFileUtilities.FixFilePath integration in AbsolutePath. /// This test simulates scenarios where intermediate path processing might /// leave backslashes in the AbsolutePath on Unix systems. /// diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 46762a8712e..03d4397e0df 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -3455,7 +3455,7 @@ public void PropertyFunctionStaticMethodDirectoryNameOfFileAbove() string result = expander.ExpandIntoStringAndUnescape(@"$([MSBuild]::GetDirectoryNameOfFileAbove($(StartingDirectory), $(FileToFind)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); - Assert.Equal(Microsoft.Build.Shared.FileUtilities.EnsureTrailingSlash(tempPath), Microsoft.Build.Shared.FileUtilities.EnsureTrailingSlash(result)); + Assert.Equal(FrameworkFileUtilities.EnsureTrailingSlash(tempPath), FrameworkFileUtilities.EnsureTrailingSlash(result)); result = expander.ExpandIntoStringAndUnescape(@"$([MSBuild]::GetDirectoryNameOfFileAbove($(StartingDirectory), Hobbits))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); diff --git a/src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs b/src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs index bcf72ce632a..77e4ffded51 100644 --- a/src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs +++ b/src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs @@ -476,7 +476,7 @@ public static IEnumerable SolutionOnlyDependenciesData } [Theory] - [MemberData(nameof(SolutionOnlyDependenciesData))] + [MemberData(nameof(SolutionOnlyDependenciesData), DisableDiscoveryEnumeration = true)] public void SolutionsCanInjectEdgesIntoTheProjectGraph(Dictionary edges, (int, int)[] solutionDependencies, bool hasCycle, bool solutionEdgesOverlapGraphEdges) { // Use the same global properties as the solution would use so all ConfigurationMetadata objects would match on global properties. diff --git a/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs b/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs index 13bb4275c65..1da40fe77d3 100644 --- a/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs +++ b/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs @@ -484,7 +484,7 @@ public static IEnumerable UndeclaredReferenceEnforcementShouldNormaliz } [Theory] - [MemberData(nameof(UndeclaredReferenceEnforcementShouldNormalizeFilePathsTestData))] + [MemberData(nameof(UndeclaredReferenceEnforcementShouldNormalizeFilePathsTestData), DisableDiscoveryEnumeration = true)] public void UndeclaredReferenceEnforcementShouldNormalizeFilePaths(Func projectReferenceModifier, Func msbuildProjectModifier, string targetName) { AssertBuild(new[] { targetName }, diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index baf7e0a1ae3..e275395f5f3 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -1804,7 +1804,7 @@ public static IEnumerable Graphs } [Theory] - [MemberData(nameof(Graphs))] + [MemberData(nameof(Graphs), DisableDiscoveryEnumeration = true)] public void TopologicalSortShouldTopologicallySort(Dictionary edges) { using (var env = TestEnvironment.Create()) @@ -1827,7 +1827,7 @@ public void TopologicalSortShouldTopologicallySort(Dictionary edges) } [Theory] - [MemberData(nameof(Graphs))] + [MemberData(nameof(Graphs), DisableDiscoveryEnumeration = true)] public void DotNotationShouldRepresentGraph(Dictionary edges) { var graph = Helpers.CreateProjectGraph( @@ -2320,7 +2320,7 @@ public static IEnumerable AllNodesShouldHaveGraphBuildGlobalPropertyDa } [Theory] - [MemberData(nameof(AllNodesShouldHaveGraphBuildGlobalPropertyData))] + [MemberData(nameof(AllNodesShouldHaveGraphBuildGlobalPropertyData), DisableDiscoveryEnumeration = true)] public void AllNodesShouldHaveGraphBuildGlobalProperty(Dictionary edges, int[] entryPoints, Dictionary globalProperties) { using (var env = TestEnvironment.Create()) @@ -2354,7 +2354,7 @@ public void UserValuesForIsGraphBuildGlobalPropertyShouldBePreserved() } [Theory] - [MemberData(nameof(Graphs))] + [MemberData(nameof(Graphs), DisableDiscoveryEnumeration = true)] public void GraphShouldSupportTransitiveReferences(Dictionary edges) { var graph = Helpers.CreateProjectGraph( @@ -2479,7 +2479,7 @@ public static IEnumerable TransitiveReferencesAreDefinedPerProjectTest } [Theory] - [MemberData(nameof(TransitiveReferencesAreDefinedPerProjectTestData))] + [MemberData(nameof(TransitiveReferencesAreDefinedPerProjectTestData), DisableDiscoveryEnumeration = true)] public void TransitiveReferencesAreDefinedPerProject( Dictionary edges, Dictionary extraContentPerProjectNumber, diff --git a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs index f9dd9bf05d4..11033d78af8 100644 --- a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs +++ b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs @@ -282,7 +282,7 @@ public static IEnumerable BuildGraphData } [Theory] - [MemberData(nameof(BuildGraphData))] + [MemberData(nameof(BuildGraphData), DisableDiscoveryEnumeration = true)] public void BuildProjectGraphUsingCaches(Dictionary edges) { var topoSortedNodes = diff --git a/src/Build.UnitTests/Instance/TaskItem_Tests.cs b/src/Build.UnitTests/Instance/TaskItem_Tests.cs index 93bb0b2caa0..51914da66d5 100644 --- a/src/Build.UnitTests/Instance/TaskItem_Tests.cs +++ b/src/Build.UnitTests/Instance/TaskItem_Tests.cs @@ -332,6 +332,7 @@ public void Escaping2() ProjectRootElement xml = projectRootElementFromString.Project; Project project = new Project(xml); + project.FullPath = "test.proj"; MockLogger logger = new MockLogger(); project.Build("Build", new ILogger[] { logger }).ShouldBeTrue(); diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 4ee55fb60ea..25ce6ea2727 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -30,10 +30,7 @@ all - - - - + diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Linux.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Linux.verified.txt index 34432ae0de0..ca72cc765ec 100644 --- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Linux.verified.txt +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Linux.verified.txt @@ -1,8 +1,9 @@ The plugin credential provider could not acquire credentials.Authentication may require manual action. Consider re-running the command with --interactive for `dotnet`, /p:NuGetInteractive="true" for MSBuild or removing the -NonInteractive switch for `NuGet` -directory/file(1,2,3,4): warning AA0000: Warning! -directory/file(1,2,3,4): warning AA0000: - A - Multi - Line - Warning! -directory/file(1,2,3,4): error AA0000: Error! + project failed with 1 error(s) and 2 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): warning AA0000: + A + Multi + Line + Warning! + directory/file(1,2,3,4): error AA0000: Error! diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.OSX.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.OSX.verified.txt index 5037f4b16db..450eaea951f 100644 --- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.OSX.verified.txt +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.OSX.verified.txt @@ -1,8 +1,9 @@ The plugin credential provider could not acquire credentials.Authentication may require manual action. Consider re-running the command with --interactive for `dotnet`, /p:NuGetInteractive="true" for MSBuild or removing the -NonInteractive switch for `NuGet` -directory/file(1,2,3,4): warning AA0000: Warning! -directory/file(1,2,3,4): warning AA0000: - A - Multi - Line - Warning! -directory/file(1,2,3,4): error AA0000: Error! + project failed with 1 error(s) and 2 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): warning AA0000: + A + Multi + Line + Warning! + directory/file(1,2,3,4): error AA0000: Error! diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Windows.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Windows.verified.txt index 34432ae0de0..ca72cc765ec 100644 --- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Windows.verified.txt +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.PrintBuildSummaryQuietVerbosity_FailedWithErrors.Windows.verified.txt @@ -1,8 +1,9 @@ The plugin credential provider could not acquire credentials.Authentication may require manual action. Consider re-running the command with --interactive for `dotnet`, /p:NuGetInteractive="true" for MSBuild or removing the -NonInteractive switch for `NuGet` -directory/file(1,2,3,4): warning AA0000: Warning! -directory/file(1,2,3,4): warning AA0000: - A - Multi - Line - Warning! -directory/file(1,2,3,4): error AA0000: Error! + project failed with 1 error(s) and 2 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): warning AA0000: + A + Multi + Line + Warning! + directory/file(1,2,3,4): error AA0000: Error! diff --git a/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs b/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs new file mode 100644 index 00000000000..b82ca9693ba --- /dev/null +++ b/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.Engine.UnitTests +{ + /// + /// End-to-end tests for task host factory lifecycle behavior. + /// + /// Tests validate the behavior based on whether the TaskHost runtime matches + /// the executing MSBuild runtime and whether TaskHostFactory is explicitly requested. + /// + /// This is a regression test for https://github.com/dotnet/msbuild/issues/13013 + /// + public class TaskHostFactoryLifecycle_E2E_Tests + { + private static string AssemblyLocation { get; } = Path.Combine(Path.GetDirectoryName(typeof(TaskHostFactoryLifecycle_E2E_Tests).Assembly.Location) ?? AppContext.BaseDirectory); + + private static string TestAssetsRootPath { get; } = Path.Combine(AssemblyLocation, "TestAssets", "TaskHostLifecycle"); + + private const string TaskHostFactory = "TaskHostFactory"; + private const string AssemblyTaskFactory = "AssemblyTaskFactory"; + private const string CurrentRuntime = "CurrentRuntime"; + private const string NetRuntime = "NET"; + + private readonly ITestOutputHelper _output; + + public TaskHostFactoryLifecycle_E2E_Tests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Validates task host lifecycle behavior for all scenarios. + /// + /// Test scenarios: + /// 1. Runtime matches + TaskHostFactory requested → short-lived out of proc (nodereuse:False) + /// 2. Runtime matches + TaskHostFactory NOT requested → in-proc execution + /// 3. Runtime doesn't match + TaskHostFactory requested → short-lived out of proc (nodereuse:False) + /// 4. Runtime doesn't match + TaskHostFactory NOT requested → long-lived sidecar out of proc (nodereuse:True) + /// + /// The runtime to use for the task (CurrentRuntime or NET) + /// The task factory to use (TaskHostFactory or AssemblyTaskFactory) + [Theory] +#if NET + [InlineData(CurrentRuntime, AssemblyTaskFactory)] // Match + No Explicit → in-proc + [InlineData(CurrentRuntime, TaskHostFactory)] // Match + Explicit → short-lived out-of-proc +#endif + [InlineData(NetRuntime, AssemblyTaskFactory)] // No Match + No Explicit → long-lived sidecar out-of-proc + [InlineData(NetRuntime, TaskHostFactory)] // No Match + Explicit → short-lived out-of-proc + public void TaskHostLifecycle_ValidatesAllScenarios( + string runtimeToUse, + string taskFactoryToUse) + { + bool? expectedNodeReuse = DetermineExpectedNodeReuse(runtimeToUse, taskFactoryToUse); + + using TestEnvironment env = TestEnvironment.Create(_output); + + string buildOutput = ExecuteBuildWithTaskHost(runtimeToUse, taskFactoryToUse); + + ValidateTaskHostBehavior(buildOutput, expectedNodeReuse); + } + + private static bool? DetermineExpectedNodeReuse(string runtimeToUse, string taskFactoryToUse) + => (taskFactoryToUse, runtimeToUse) switch + { + // TaskHostFactory is always short-lived and out-of-proc (nodereuse:False) + (TaskHostFactory, _) => false, + + // AssemblyTaskFactory with CurrentRuntime runs in-proc + (AssemblyTaskFactory, CurrentRuntime) => null, + + // AssemblyTaskFactory with NET runtime: + // - On .NET Framework host: out-of-proc with long-lived sidecar (nodereuse:True) + // - On .NET host: in-proc + (AssemblyTaskFactory, NetRuntime) => +#if NET + null, // On .NET host: in-proc execution +#else + true, // On .NET Framework host: out-of-proc with long-lived sidecar +#endif + _ => throw new ArgumentException($"Unknown combination: runtime={runtimeToUse}, factory={taskFactoryToUse}") + }; + + private string ExecuteBuildWithTaskHost(string runtimeToUse, string taskFactoryToUse) + { + string testProjectPath = Path.Combine(TestAssetsRootPath, "TaskHostLifecycleTestApp.csproj"); + + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"{testProjectPath} -v:n -restore /p:RuntimeToUse={runtimeToUse} /p:TaskFactoryToUse={taskFactoryToUse}", + out bool success, + outputHelper: _output); + + success.ShouldBeTrue("Build should succeed"); + + return output; + } + + private static void ValidateTaskHostBehavior(string buildOutput, bool? expectedNodeReuse) + { + if (expectedNodeReuse.HasValue) + { + buildOutput.ShouldContain("/nodemode:", customMessage: "Task should run out-of-proc and have /nodemode: in its command-line arguments"); + + string expectedFlag = expectedNodeReuse.Value ? "/nodereuse:True" : "/nodereuse:False"; + buildOutput.ShouldContain(expectedFlag, customMessage: $"Task should have {expectedFlag} in its command-line arguments"); + } + else + { + buildOutput.ShouldNotContain("/nodemode:", customMessage: "Task should run in-proc and not have task host command-line arguments like /nodemode:"); + } + } + } +} diff --git a/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs b/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs deleted file mode 100644 index 7a567e79495..00000000000 --- a/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.Build.Framework.Telemetry; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Engine.UnitTests.Telemetry -{ - public class ActivityExtensionsTests - { - [Fact] - public void WithTag_ShouldSetUnhashedValue() - { - var activity = new Activity("TestActivity"); - activity.Start(); - - var telemetryItem = new TelemetryItem( - Name: "TestItem", - Value: "TestValue", - NeedsHashing: false); - - activity.WithTag(telemetryItem); - - var tagValue = activity.GetTagItem("VS.MSBuild.TestItem"); - tagValue.ShouldNotBeNull(); - tagValue.ShouldBe("TestValue"); - - activity.Dispose(); - } - - [Fact] - public void WithTag_ShouldSetHashedValue() - { - var activity = new Activity("TestActivity"); - var telemetryItem = new TelemetryItem( - Name: "TestItem", - Value: "SensitiveValue", - NeedsHashing: true); - - activity.WithTag(telemetryItem); - - var tagValue = activity.GetTagItem("VS.MSBuild.TestItem"); - tagValue.ShouldNotBeNull(); - tagValue.ShouldNotBe("SensitiveValue"); // Ensure it’s not the plain text - activity.Dispose(); - } - - [Fact] - public void WithTags_ShouldSetMultipleTags() - { - var activity = new Activity("TestActivity"); - var tags = new List - { - new("Item1", "Value1", false), - new("Item2", "Value2", true) // hashed - }; - - activity.WithTags(tags); - - var tagValue1 = activity.GetTagItem("VS.MSBuild.Item1"); - var tagValue2 = activity.GetTagItem("VS.MSBuild.Item2"); - - tagValue1.ShouldNotBeNull(); - tagValue1.ShouldBe("Value1"); - - tagValue2.ShouldNotBeNull(); - tagValue2.ShouldNotBe("Value2"); // hashed - - activity.Dispose(); - } - - [Fact] - public void WithTags_DataHolderShouldSetMultipleTags() - { - var activity = new Activity("TestActivity"); - var dataHolder = new MockTelemetryDataHolder(); // see below - - activity.WithTags(dataHolder); - - var tagValueA = activity.GetTagItem("VS.MSBuild.TagA"); - var tagValueB = activity.GetTagItem("VS.MSBuild.TagB"); - - tagValueA.ShouldNotBeNull(); - tagValueA.ShouldBe("ValueA"); - - tagValueB.ShouldNotBeNull(); - tagValueB.ShouldNotBe("ValueB"); // should be hashed - activity.Dispose(); - } - - [Fact] - public void WithStartTime_ShouldSetActivityStartTime() - { - var activity = new Activity("TestActivity"); - var now = DateTime.UtcNow; - - activity.WithStartTime(now); - - activity.StartTimeUtc.ShouldBe(now); - activity.Dispose(); - } - - [Fact] - public void WithStartTime_NullDateTime_ShouldNotSetStartTime() - { - var activity = new Activity("TestActivity"); - var originalStartTime = activity.StartTimeUtc; // should be default (min) if not started - - activity.WithStartTime(null); - - activity.StartTimeUtc.ShouldBe(originalStartTime); - - activity.Dispose(); - } - } - - /// - /// A simple mock for testing IActivityTelemetryDataHolder. - /// Returns two items: one hashed, one not hashed. - /// - internal sealed class MockTelemetryDataHolder : IActivityTelemetryDataHolder - { - public IList GetActivityProperties() - { - return new List - { - new("TagA", "ValueA", false), - new("TagB", "ValueB", true), - }; - } - } - - - public class MSBuildActivitySourceTests - { - [Fact] - public void StartActivity_ShouldPrefixNameCorrectly_WhenNoRemoteParent() - { - var source = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, 1.0); - using var listener = new ActivityListener - { - ShouldListenTo = activitySource => activitySource.Name == TelemetryConstants.DefaultActivitySourceNamespace, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - ActivitySource.AddActivityListener(listener); - - - var activity = source.StartActivity("Build"); - - activity.ShouldNotBeNull(); - activity?.DisplayName.ShouldBe("VS/MSBuild/Build"); - - activity?.Dispose(); - } - - [Fact] - public void StartActivity_ShouldUseParentId_WhenRemoteParentExists() - { - // Arrange - var parentActivity = new Activity("ParentActivity"); - parentActivity.SetParentId("|12345.abcde."); // Simulate some parent trace ID - parentActivity.AddTag("sampleTag", "sampleVal"); - parentActivity.Start(); - - var source = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, 1.0); - using var listener = new ActivityListener - { - ShouldListenTo = activitySource => activitySource.Name == TelemetryConstants.DefaultActivitySourceNamespace, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - ActivitySource.AddActivityListener(listener); - - // Act - var childActivity = source.StartActivity("ChildBuild"); - - // Assert - childActivity.ShouldNotBeNull(); - // If HasRemoteParent is true, the code uses `parentId: Activity.Current.ParentId`. - // However, by default .NET Activity doesn't automatically set HasRemoteParent = true - // unless you explicitly set it. If you have logic that sets it, you can test it here. - // For demonstration, we assume the ParentId is carried over if HasRemoteParent == true. - if (Activity.Current?.HasRemoteParent == true) - { - childActivity?.ParentId.ShouldBe("|12345.abcde."); - } - - parentActivity.Dispose(); - childActivity?.Dispose(); - } - } -} diff --git a/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs b/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs deleted file mode 100644 index 3faa3ab54a9..00000000000 --- a/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.Build.Execution; -using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Engine.UnitTests.Telemetry -{ - // Putting the tests to a collection ensures tests run serially by default, that's needed to isolate the manager singleton state and env vars in some telemetry tests. - [Collection("OpenTelemetryManagerTests")] - public class OpenTelemetryManagerTests : IDisposable - { - - private const string TelemetryFxOptoutEnvVarName = "MSBUILD_TELEMETRY_OPTOUT"; - private const string DotnetOptOut = "DOTNET_CLI_TELEMETRY_OPTOUT"; - private const string TelemetrySampleRateOverrideEnvVarName = "MSBUILD_TELEMETRY_SAMPLE_RATE"; - private const string VS1714TelemetryOptInEnvVarName = "MSBUILD_TELEMETRY_OPTIN"; - - public OpenTelemetryManagerTests() - { - } - - public void Dispose() - { - ResetManagerState(); - } - - [Theory] - [InlineData(DotnetOptOut, "true")] - [InlineData(TelemetryFxOptoutEnvVarName, "true")] - [InlineData(DotnetOptOut, "1")] - [InlineData(TelemetryFxOptoutEnvVarName, "1")] - public void Initialize_ShouldSetStateToOptOut_WhenOptOutEnvVarIsTrue(string optoutVar, string value) - { - // Arrange - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(optoutVar, value); - - // Act - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - - // Assert - OpenTelemetryManager.Instance.IsActive().ShouldBeFalse(); - } - -#if NETCOREAPP - [Fact] - public void Initialize_ShouldSetStateToUnsampled_WhenNoOverrideOnNetCore() - { - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, null); - environment.SetEnvironmentVariable(DotnetOptOut, null); - - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - - // If no override on .NET, we expect no Active ActivitySource - OpenTelemetryManager.Instance.DefaultActivitySource.ShouldBeNull(); - } -#endif - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Initialize_ShouldSetSampleRateOverride_AndCreateActivitySource_WhenRandomBelowOverride(bool standalone) - { - // Arrange - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(VS1714TelemetryOptInEnvVarName, "1"); - environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, "1.0"); - environment.SetEnvironmentVariable(DotnetOptOut, null); - - // Act - OpenTelemetryManager.Instance.Initialize(isStandalone: standalone); - - // Assert - OpenTelemetryManager.Instance.IsActive().ShouldBeTrue(); - OpenTelemetryManager.Instance.DefaultActivitySource.ShouldNotBeNull(); - } - - [Fact] - public void Initialize_ShouldNoOp_WhenCalledMultipleTimes() - { - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(DotnetOptOut, "true"); - OpenTelemetryManager.Instance.Initialize(isStandalone: true); - var state1 = OpenTelemetryManager.Instance.IsActive(); - - environment.SetEnvironmentVariable(DotnetOptOut, null); - OpenTelemetryManager.Instance.Initialize(isStandalone: true); - var state2 = OpenTelemetryManager.Instance.IsActive(); - - // Because the manager is already initialized, second call is a no-op - state1.ShouldBe(false); - state2.ShouldBe(false); - } - - [Fact] - public void TelemetryLoadFailureIsLoggedOnce() - { - OpenTelemetryManager.Instance.LoadFailureExceptionMessage = new System.IO.FileNotFoundException().ToString(); - using BuildManager bm = new BuildManager(); - var deferredMessages = new List(); - bm.BeginBuild(new BuildParameters(), deferredMessages); - deferredMessages.ShouldContain(x => x.Text.Contains("FileNotFound")); - bm.EndBuild(); - bm.BeginBuild(new BuildParameters()); - bm.EndBuild(); - - // should not add message twice - int count = deferredMessages.Count(x => x.Text.Contains("FileNotFound")); - count.ShouldBe(1); - } - - /* Helper methods */ - - /// - /// Resets the singleton manager to a known uninitialized state so each test is isolated. - /// - private void ResetManagerState() - { - var instance = OpenTelemetryManager.Instance; - - // 1. Reset the private _telemetryState field - var telemetryStateField = typeof(OpenTelemetryManager) - .GetField("_telemetryState", BindingFlags.NonPublic | BindingFlags.Instance); - telemetryStateField?.SetValue(instance, OpenTelemetryManager.TelemetryState.Uninitialized); - - // 2. Null out the DefaultActivitySource property - var defaultSourceProp = typeof(OpenTelemetryManager) - .GetProperty(nameof(OpenTelemetryManager.DefaultActivitySource), - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - defaultSourceProp?.SetValue(instance, null); - } - } -} diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index f03ee221094..d81776f1d77 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text.Json; +using System.Threading; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; @@ -14,10 +14,12 @@ using Shouldly; using Xunit; using Xunit.Abstractions; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; +using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; namespace Microsoft.Build.Engine.UnitTests { - [Collection("OpenTelemetryManagerTests")] + [Collection("TelemetryManagerTests")] public class Telemetry_Tests { private readonly ITestOutputHelper _output; @@ -27,29 +29,6 @@ public Telemetry_Tests(ITestOutputHelper output) _output = output; } - private sealed class ProjectFinishedCapturingLogger : ILogger - { - private readonly List _projectFinishedEventArgs = []; - public LoggerVerbosity Verbosity { get; set; } - public string? Parameters { get; set; } - - public IReadOnlyList ProjectFinishedEventArgsReceived => - _projectFinishedEventArgs; - - public void Initialize(IEventSource eventSource) - { - eventSource.ProjectFinished += EventSource_ProjectFinished; - } - - private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) - { - _projectFinishedEventArgs.Add(e); - } - - public void Shutdown() - { } - } - [Fact] public void WorkerNodeTelemetryCollection_BasicTarget() { @@ -57,16 +36,16 @@ public void WorkerNodeTelemetryCollection_BasicTarget() InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; var testProject = """ - - - - - - - - - - """; + + + + + + + + + + """; MockLogger logger = new MockLogger(_output); Helpers.BuildProjectContentUsingBuildManager(testProject, logger, @@ -75,13 +54,13 @@ public void WorkerNodeTelemetryCollection_BasicTarget() workerNodeTelemetryData!.ShouldNotBeNull(); var buildTargetKey = new TaskOrTargetTelemetryKey("Build", true, false); workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(buildTargetKey); - workerNodeTelemetryData.TargetsExecutionData[buildTargetKey].ShouldBeTrue(); + workerNodeTelemetryData.TargetsExecutionData[buildTargetKey].WasExecuted.ShouldBeTrue(); workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(1); workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(2); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(2); workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsCustom && !k.IsNuget); @@ -92,8 +71,8 @@ public void WorkerNodeTelemetryCollection_BasicTarget() [Fact] public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() { - WorkerNodeTelemetryData? workerNodeTelemetryData = null; - InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; + WorkerNodeTelemetryData? workerNodeData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; var testProject = """ @@ -108,7 +87,6 @@ public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() - - @@ -129,73 +106,161 @@ public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() - - """; + MockLogger logger = new MockLogger(_output); - Helpers.BuildProjectContentUsingBuildManager(testProject, logger, + Helpers.BuildProjectContentUsingBuildManager( + testProject, + logger, new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); - workerNodeTelemetryData!.ShouldNotBeNull(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); - workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(3); + workerNodeData!.ShouldNotBeNull(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].WasExecuted.ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].WasExecuted.ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].WasExecuted.ShouldBeFalse(); + workerNodeData.TargetsExecutionData.Keys.Count.ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(3); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount).ShouldBe(2); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount.ShouldBe(2); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount).ShouldBe(0); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount.ShouldBe(0); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); - workerNodeTelemetryData.TasksExecutionData.Values - .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); + workerNodeData.TasksExecutionData.Values.Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); + workerNodeData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); + } + + [Fact] + public void WorkerNodeTelemetryCollection_TaskFactoryName() + { + WorkerNodeTelemetryData? workerNodeData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; + + var testProject = """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello from inline task!"); + + + + + + + + + """; + + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager( + testProject, + logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeData!.ShouldNotBeNull(); + + // Verify built-in task has AssemblyTaskFactory + var messageTaskKey = (TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"; + workerNodeData.TasksExecutionData.ShouldContainKey(messageTaskKey); + workerNodeData.TasksExecutionData[messageTaskKey].TaskFactoryName.ShouldBe("AssemblyTaskFactory"); + + // Verify inline task has RoslynCodeTaskFactory + var inlineTaskKey = new TaskOrTargetTelemetryKey("InlineTask01", true, false); + workerNodeData.TasksExecutionData.ShouldContainKey(inlineTaskKey); + workerNodeData.TasksExecutionData[inlineTaskKey].TaskFactoryName.ShouldBe("RoslynCodeTaskFactory"); + workerNodeData.TasksExecutionData[inlineTaskKey].ExecutionsCount.ShouldBe(1); + } + + [Fact] + public void TelemetryDataUtils_HashesCustomFactoryName() + { + // Create telemetry data with a custom factory name + var tasksData = new Dictionary + { + { new TaskOrTargetTelemetryKey("CustomTask", true, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(100), 1, 1000, "MyCompany.CustomTaskFactory", null) }, + { new TaskOrTargetTelemetryKey("BuiltInTask", false, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(50), 2, 500, "AssemblyTaskFactory", null) }, + { new TaskOrTargetTelemetryKey("InlineTask", true, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(75), 1, 750, "RoslynCodeTaskFactory", "CLR4") } + }; + var targetsData = new Dictionary(); + var telemetryData = new WorkerNodeTelemetryData(tasksData, targetsData); + + var activityData = telemetryData.AsActivityDataHolder(includeTasksDetails: true, includeTargetDetails: false); + activityData.ShouldNotBeNull(); + + var properties = activityData.GetActivityProperties(); + properties.ShouldContainKey("Tasks"); + + var taskDetails = properties["Tasks"] as List; + taskDetails.ShouldNotBeNull(); + + // Custom factory name should be hashed + var customTask = taskDetails!.FirstOrDefault(t => t.IsCustom && t.Name != GetHashed("InlineTask")); + customTask.ShouldNotBeNull(); + customTask!.FactoryName.ShouldBe(GetHashed("MyCompany.CustomTaskFactory")); + + // Known factory names should NOT be hashed + var builtInTask = taskDetails.FirstOrDefault(t => !t.IsCustom); + builtInTask.ShouldNotBeNull(); + builtInTask!.FactoryName.ShouldBe("AssemblyTaskFactory"); + + var inlineTask = taskDetails.FirstOrDefault(t => t.FactoryName == "RoslynCodeTaskFactory"); + inlineTask.ShouldNotBeNull(); + inlineTask!.FactoryName.ShouldBe("RoslynCodeTaskFactory"); + inlineTask.TaskHostRuntime.ShouldBe("CLR4"); } #if NET - // test in .net core with opentelemetry opted in to avoid sending it but enable listening to it + // test in .net core with telemetry opted in to avoid sending it but enable listening to it [Fact] public void NodeTelemetryE2E() { using TestEnvironment env = TestEnvironment.Create(); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE", "1.0"); env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); - // Reset the OpenTelemetryManager state to ensure clean test - ResetManagerState(); - - // track activities through an ActivityListener var capturedActivities = new List(); + using var activityStoppedEvent = new ManualResetEventSlim(false); using var listener = new ActivityListener { ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = capturedActivities.Add, - ActivityStopped = _ => { } + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = a => { lock (capturedActivities) { capturedActivities.Add(a); } }, + ActivityStopped = a => + { + if (a.DisplayName == "VS/MSBuild/Build") + { + activityStoppedEvent.Set(); + } + }, }; ActivitySource.AddActivityListener(listener); + // Reset TelemetryManager to force re-initialization with our listener active + TelemetryManager.ResetForTest(); + var testProject = @" @@ -248,78 +313,162 @@ public void NodeTelemetryE2E() // Phase 3: End Build - This puts telemetry to an system.diagnostics activity buildManager.EndBuild(); - - // Verify build activity were captured by the listener and contain task and target info - capturedActivities.ShouldNotBeEmpty(); - var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); - var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); - tags.ShouldNotBeNull(); - - tags.ShouldContainKey("VS.MSBuild.BuildTarget"); - tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); - - // Verify task data - tags.ShouldContainKey("VS.MSBuild.Tasks"); - var tasksJson = tags["VS.MSBuild.Tasks"]; - tasksJson.ShouldNotBeNullOrEmpty(); - tasksJson.ShouldContain("Microsoft.Build.Tasks.Message"); - tasksJson.ShouldContain("Microsoft.Build.Tasks.CreateItem"); - - // Parse tasks data for detailed assertions - var tasksData = JsonSerializer.Deserialize(tasksJson); - - // Verify Message task execution metrics - updated for object structure - tasksData.TryGetProperty("Microsoft.Build.Tasks.Message", out var messageTask).ShouldBe(true); - messageTask.GetProperty("ExecutionsCount").GetInt32().ShouldBe(3); - messageTask.GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - messageTask.GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); - messageTask.GetProperty(nameof(TaskOrTargetTelemetryKey.IsCustom)).GetBoolean().ShouldBe(false); - messageTask.GetProperty(nameof(TaskOrTargetTelemetryKey.IsCustom)).GetBoolean().ShouldBe(false); - - // Verify CreateItem task execution metrics - updated for object structure - tasksData.TryGetProperty("Microsoft.Build.Tasks.CreateItem", out var createItemTask).ShouldBe(true); - createItemTask.GetProperty("ExecutionsCount").GetInt32().ShouldBe(1); - createItemTask.GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - createItemTask.GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); - - // Verify Targets summary information - tags.ShouldContainKey("VS.MSBuild.TargetsSummary"); - var targetsSummaryJson = tags["VS.MSBuild.TargetsSummary"]; - targetsSummaryJson.ShouldNotBeNullOrEmpty(); - var targetsSummary = JsonSerializer.Deserialize(targetsSummaryJson); - - // Verify loaded and executed targets counts - match structure in TargetsSummaryConverter.Write - targetsSummary.GetProperty("Loaded").GetProperty("Total").GetInt32().ShouldBe(2); - targetsSummary.GetProperty("Executed").GetProperty("Total").GetInt32().ShouldBe(2); - - // Verify Tasks summary information - tags.ShouldContainKey("VS.MSBuild.TasksSummary"); - var tasksSummaryJson = tags["VS.MSBuild.TasksSummary"]; - tasksSummaryJson.ShouldNotBeNullOrEmpty(); - var tasksSummary = JsonSerializer.Deserialize(tasksSummaryJson); - - // Verify task execution summary metrics based on TasksSummaryConverter.Write structure - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("ExecutionsCount").GetInt32().ShouldBe(4); - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); } - // Reset the OpenTelemetryManager state to ensure it doesn't affect other tests - ResetManagerState(); + + // Wait for the activity to be fully processed + activityStoppedEvent.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue("Timed out waiting for build activity to stop"); + + // Verify build activity were captured by the listener and contain task and target info + capturedActivities.ShouldNotBeEmpty(); + var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); + var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); + tags.ShouldNotBeNull(); + + tags.ShouldContainKey("VS.MSBuild.BuildTarget"); + tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); + + // Verify task data + var tasks = activity.TagObjects.FirstOrDefault(to => to.Key == "VS.MSBuild.Tasks"); + + var tasksData = tasks.Value as List; + var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); + messageTaskData.ShouldNotBeNull(); + + // Verify Message task execution metrics + messageTaskData.ExecutionsCount.ShouldBe(3); + messageTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + messageTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + messageTaskData.IsCustom.ShouldBe(false); + + // Verify CreateItem task execution metrics + var createItemTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.CreateItem"); + createItemTaskData.ShouldNotBeNull(); + createItemTaskData.ExecutionsCount.ShouldBe(1); + createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + + // Verify TaskFactoryName is populated for built-in tasks + messageTaskData.FactoryName.ShouldBe("AssemblyTaskFactory"); + createItemTaskData.FactoryName.ShouldBe("AssemblyTaskFactory"); + + // Verify Targets summary information + var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); + var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; + targetsSummary.ShouldNotBeNull(); + targetsSummary.Loaded.Total.ShouldBe(2); + targetsSummary.Executed.Total.ShouldBe(2); + + // Verify Tasks summary information + var tasksSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TasksSummary")); + var tasksSummary = tasksSummaryTagObject.Value as TasksSummaryInfo; + tasksSummary.ShouldNotBeNull(); + + tasksSummary.Microsoft.ShouldNotBeNull(); + tasksSummary.Microsoft!.Total!.ExecutionsCount.ShouldBe(4); + tasksSummary.Microsoft!.Total!.TotalMilliseconds.ShouldBeGreaterThan(0); + + // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. + tasksSummary.Microsoft.Total.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); } +#endif - private void ResetManagerState() + private sealed class ProjectFinishedCapturingLogger : ILogger { - var instance = OpenTelemetryManager.Instance; - typeof(OpenTelemetryManager) - .GetField("_telemetryState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(instance, OpenTelemetryManager.TelemetryState.Uninitialized); - - typeof(OpenTelemetryManager) - .GetProperty("DefaultActivitySource", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(instance, null); + private readonly List _projectFinishedEventArgs = []; + + public LoggerVerbosity Verbosity { get; set; } + + public string? Parameters { get; set; } + + public IReadOnlyList ProjectFinishedEventArgsReceived => _projectFinishedEventArgs; + + public void Initialize(IEventSource eventSource) => eventSource.ProjectFinished += EventSource_ProjectFinished; + + private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) => _projectFinishedEventArgs.Add(e); + + public void Shutdown() { } + } + + [Fact] + public void BuildIncrementalityInfo_AllTargetsExecuted_ClassifiedAsFull() + { + // Arrange: All targets were executed (none skipped) + var targetsData = new Dictionary + { + { new TaskOrTargetTelemetryKey("Build", false, false), TargetExecutionStats.Executed() }, + { new TaskOrTargetTelemetryKey("Compile", false, false), TargetExecutionStats.Executed() }, + { new TaskOrTargetTelemetryKey("Link", false, false), TargetExecutionStats.Executed() }, + { new TaskOrTargetTelemetryKey("Pack", false, false), TargetExecutionStats.Executed() }, + }; + var tasksData = new Dictionary(); + var telemetryData = new WorkerNodeTelemetryData(tasksData, targetsData); + + // Act + var activityData = telemetryData.AsActivityDataHolder(includeTasksDetails: false, includeTargetDetails: false); + var properties = activityData!.GetActivityProperties(); + + // Assert + properties.ShouldContainKey("Incrementality"); + var incrementality = properties["Incrementality"] as BuildInsights.BuildIncrementalityInfo; + incrementality.ShouldNotBeNull(); + incrementality!.Classification.ShouldBe(BuildInsights.BuildType.Full); + incrementality.TotalTargetsCount.ShouldBe(4); + incrementality.ExecutedTargetsCount.ShouldBe(4); + incrementality.SkippedTargetsCount.ShouldBe(0); + incrementality.IncrementalityRatio.ShouldBe(0.0); + } + + [Fact] + public void BuildIncrementalityInfo_MostTargetsSkipped_ClassifiedAsIncremental() + { + // Arrange: Most targets were skipped (>70%) + var targetsData = new Dictionary + { + { new TaskOrTargetTelemetryKey("Build", false, false), TargetExecutionStats.Skipped(TargetSkipReason.OutputsUpToDate) }, + { new TaskOrTargetTelemetryKey("Compile", false, false), TargetExecutionStats.Skipped(TargetSkipReason.OutputsUpToDate) }, + { new TaskOrTargetTelemetryKey("Link", false, false), TargetExecutionStats.Skipped(TargetSkipReason.ConditionWasFalse) }, + { new TaskOrTargetTelemetryKey("Pack", false, false), TargetExecutionStats.Executed() }, // Only one executed + }; + var tasksData = new Dictionary(); + var telemetryData = new WorkerNodeTelemetryData(tasksData, targetsData); + + // Act + var activityData = telemetryData.AsActivityDataHolder(includeTasksDetails: false, includeTargetDetails: false); + var properties = activityData!.GetActivityProperties(); + + // Assert + properties.ShouldContainKey("Incrementality"); + var incrementality = properties["Incrementality"] as BuildInsights.BuildIncrementalityInfo; + incrementality.ShouldNotBeNull(); + incrementality!.Classification.ShouldBe(BuildInsights.BuildType.Incremental); + incrementality.TotalTargetsCount.ShouldBe(4); + incrementality.ExecutedTargetsCount.ShouldBe(1); + incrementality.SkippedTargetsCount.ShouldBe(3); + incrementality.SkippedDueToUpToDateCount.ShouldBe(2); + incrementality.SkippedDueToConditionCount.ShouldBe(1); + incrementality.SkippedDueToPreviouslyBuiltCount.ShouldBe(0); + incrementality.IncrementalityRatio.ShouldBe(0.75); + } + + [Fact] + public void BuildIncrementalityInfo_NoTargets_ClassifiedAsUnknown() + { + // Arrange: No targets + var targetsData = new Dictionary(); + var tasksData = new Dictionary(); + var telemetryData = new WorkerNodeTelemetryData(tasksData, targetsData); + + // Act + var activityData = telemetryData.AsActivityDataHolder(includeTasksDetails: false, includeTargetDetails: false); + var properties = activityData!.GetActivityProperties(); + + // Assert + properties.ShouldContainKey("Incrementality"); + var incrementality = properties["Incrementality"] as BuildInsights.BuildIncrementalityInfo; + incrementality.ShouldNotBeNull(); + incrementality!.Classification.ShouldBe(BuildInsights.BuildType.Unknown); + incrementality.TotalTargetsCount.ShouldBe(0); + incrementality.IncrementalityRatio.ShouldBe(0.0); } -#endif } } diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs index 575b727ad46..59d55089d7b 100644 --- a/src/Build.UnitTests/TerminalLogger_Tests.cs +++ b/src/Build.UnitTests/TerminalLogger_Tests.cs @@ -916,11 +916,11 @@ public void TestTerminalLoggerTogetherWithOtherLoggers() string logFileWithoutTL = env.ExpectFile(".binlog").Path; // Execute MSBuild with binary, file and terminal loggers - RunnerUtilities.ExecMSBuild($"{projectFile.Path} /m /bl:{logFileWithTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithTL.log")};verbosity=diagnostic -tl:on", out bool success, outputHelper: _outputHelper); + RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithTL.log")};verbosity=diagnostic -tl:on", out bool success, outputHelper: _outputHelper); success.ShouldBeTrue(); // Execute MSBuild with binary and file loggers - RunnerUtilities.ExecMSBuild($"{projectFile.Path} /m /bl:{logFileWithoutTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithoutTL.log")};verbosity=diagnostic", out success, outputHelper: _outputHelper); + RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithoutTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithoutTL.log")};verbosity=diagnostic", out success, outputHelper: _outputHelper); success.ShouldBeTrue(); // Read the binary log and replay into mockLogger @@ -935,10 +935,10 @@ public void TestTerminalLoggerTogetherWithOtherLoggers() binaryLogReaderWithTL.Replay(logFileWithTL); binaryLogReaderWithoutTL.Replay(logFileWithoutTL); - // Check that amount of events, warnings, errors is equal in both cases. Presence of other loggers should not change behavior + // Check that amount of warnings and errors is equal in both cases. Presence of other loggers should not change behavior mockLogFromPlaybackWithoutTL.Errors.Count.ShouldBe(mockLogFromPlaybackWithTL.Errors.Count); mockLogFromPlaybackWithoutTL.Warnings.Count.ShouldBe(mockLogFromPlaybackWithTL.Warnings.Count); - mockLogFromPlaybackWithoutTL.AllBuildEvents.Count.ShouldBe(mockLogFromPlaybackWithTL.AllBuildEvents.Count); + // Note: We don't compare AllBuildEvents.Count because internal events can vary between runs and with different logger configurations // Check presence of some items and properties and that they have at least 1 item and property mockLogFromPlaybackWithoutTL.EvaluationFinishedEvents.ShouldContain(x => (x.Items != null) && x.Items.GetEnumerator().MoveNext()); diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.cs b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.cs index 563724a07f7..1a321e328cd 100644 --- a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.cs +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.cs @@ -20,7 +20,7 @@ public override bool Execute() var executingProcess = currentProcess.ProcessName; var processPath = currentProcess.MainModule?.FileName ?? "Unknown"; - Log.LogMessage(MessageImportance.High, $"The task is executed in process: {executingProcess}"); + Log.LogMessage(MessageImportance.High, $"The task is executed in process: {executingProcess} with id {currentProcess.Id}"); Log.LogMessage(MessageImportance.High, $"Process path: {processPath}"); string[] args = Environment.GetCommandLineArgs(); diff --git a/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj b/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj new file mode 100644 index 00000000000..adb43e0de99 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + $([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(AssemblyLocation)', '..'))')) + $([System.IO.Path]::Combine('$(TestProjectFolder)', '$(TargetFramework)', 'ExampleTask.dll')) + + + + + + + + + diff --git a/src/Build.UnitTests/Utilities_Tests.cs b/src/Build.UnitTests/Utilities_Tests.cs index a29466e852c..880fbe49e59 100644 --- a/src/Build.UnitTests/Utilities_Tests.cs +++ b/src/Build.UnitTests/Utilities_Tests.cs @@ -80,17 +80,9 @@ public void CommentsInPreprocessing() env.SetEnvironmentVariable("MSBUILDLOADALLFILESASWRITEABLE", "1"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe """ + inputFile.Path + - (NativeMethodsShared.IsUnixLike ? @""" -pp:""" : @""" /pp:""") + outputFile.Path + @"""") - .ShouldBe(MSBuildApp.ExitType.Success); -#else Assert.Equal( MSBuildApp.ExitType.Success, - MSBuildApp.Execute( - new[] { @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', - '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'})); -#endif + MSBuildApp.Execute([ @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'])); bool foundDoNotModify = false; foreach (string line in File.ReadLines(outputFile.Path)) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 7a70791f17b..02b2799a972 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -26,13 +26,13 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Experimental.BuildCheck.Infrastructure; -using Microsoft.Build.ProjectCache; using Microsoft.Build.FileAccesses; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Logging; +using Microsoft.Build.ProjectCache; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; @@ -280,7 +280,7 @@ public class BuildManager : INodePacketHandler, IBuildComponentHost, IDisposable /// /// Creates a new unnamed build manager. /// Normally there is only one build manager in a process, and it is the default build manager. - /// Access it with + /// Access it with . /// public BuildManager() : this("Unnamed") @@ -290,11 +290,12 @@ public BuildManager() /// /// Creates a new build manager with an arbitrary distinct name. /// Normally there is only one build manager in a process, and it is the default build manager. - /// Access it with + /// Access it with . /// public BuildManager(string hostName) { ErrorUtilities.VerifyThrowArgumentNull(hostName); + _hostName = hostName; _buildManagerState = BuildManagerState.Idle; _buildSubmissions = new Dictionary(); @@ -336,12 +337,12 @@ private enum BuildManagerState /// /// This is the state the BuildManager is in after has been called but before has been called. - /// , , , , and may be called in this state. + /// , , , , and may be called in this state. /// Building, /// - /// This is the state the BuildManager is in after has been called but before all existing submissions have completed. + /// This is the state the BuildManager is in after has been called but before all existing submissions have completed. /// WaitingForBuildToComplete } @@ -395,6 +396,16 @@ public static BuildManager DefaultBuildManager /// LegacyThreadingData IBuildComponentHost.LegacyThreadingData => _legacyThreadingData; + /// + /// Enumeration describing the severity of a deferred build message. + /// + public enum DeferredBuildMessageSeverity + { + Message = 1, + Warning, + Error + } + /// /// /// @@ -406,11 +417,19 @@ public readonly struct DeferredBuildMessage public string? FilePath { get; } + public DeferredBuildMessageSeverity MessageSeverity { get; } = DeferredBuildMessageSeverity.Message; + + /// + /// Build event code (e.g., "MSB1070"). + /// + public string? Code { get; } + public DeferredBuildMessage(string text, MessageImportance importance) { Importance = importance; Text = text; FilePath = null; + Code = null; } public DeferredBuildMessage(string text, MessageImportance importance, string filePath) @@ -418,6 +437,22 @@ public DeferredBuildMessage(string text, MessageImportance importance, string fi Importance = importance; Text = text; FilePath = filePath; + Code = null; + } + + /// + /// Creates a deferred warning message. + /// + /// The warning message text. + /// The build message code (e.g., "MSB1070"). + /// The severity of the deferred build message. + public DeferredBuildMessage(string text, string code, DeferredBuildMessageSeverity messageSeverity) + { + Importance = MessageImportance.Normal; + Text = text; + FilePath = null; + Code = code; + MessageSeverity = messageSeverity; } } @@ -459,8 +494,11 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) /// Thrown if a build is already in progress. public void BeginBuild(BuildParameters parameters) { - InitializeTelemetry(); - +#if NETFRAMEWORK + // Collect telemetry unless explicitly opted out via environment variable. + // The decision to send telemetry is made at EndBuild to avoid eager loading of telemetry assemblies. + parameters.IsTelemetryEnabled |= !TelemetryManager.IsOptOut(); +#endif if (_previousLowPriority != null) { if (parameters.LowPriority != _previousLowPriority) @@ -529,6 +567,7 @@ public void BeginBuild(BuildParameters parameters) } _buildTelemetry.InnerStartAt = now; + _buildTelemetry.IsStandaloneExecution ??= false; if (BuildParameters.DumpOpportunisticInternStats) { @@ -585,7 +624,6 @@ public void BeginBuild(BuildParameters parameters) // Initialize components. _nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager; - _buildParameters.IsTelemetryEnabled |= OpenTelemetryManager.Instance.IsActive(); var loggingService = InitializeLoggingService(); // Log deferred messages and response files @@ -739,25 +777,6 @@ void InitializeCaches() } } - private void InitializeTelemetry() - { - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - string? failureMessage = OpenTelemetryManager.Instance.LoadFailureExceptionMessage; - if (_deferredBuildMessages != null && - failureMessage != null && - _deferredBuildMessages is ICollection deferredBuildMessagesCollection) - { - deferredBuildMessagesCollection.Add( - new DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "OpenTelemetryLoadFailed", - failureMessage), - MessageImportance.Low)); - - // clean up the message from OpenTelemetryManager to avoid double logging it - OpenTelemetryManager.Instance.LoadFailureExceptionMessage = null; - } - } #if FEATURE_REPORTFILEACCESSES /// @@ -1107,6 +1126,12 @@ public void EndBuild() _buildTelemetry.BuildEngineDisplayVersion = ProjectCollection.DisplayVersion; _buildTelemetry.BuildEngineFrameworkName = NativeMethodsShared.FrameworkName; + // Populate error categorization data from the logging service + if (!_overallBuildSuccess) + { + loggingService.PopulateBuildTelemetryWithErrors(_buildTelemetry); + } + string? host = null; if (BuildEnvironmentState.s_runningInVisualStudio) { @@ -1120,6 +1145,7 @@ public void EndBuild() { host = "VSCode"; } + _buildTelemetry.BuildEngineHost = host; _buildTelemetry.BuildCheckEnabled = _buildParameters!.IsBuildCheckEnabled; @@ -1129,10 +1155,8 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); - if (OpenTelemetryManager.Instance.IsActive()) - { - EndBuildTelemetry(); - } + + EndBuildTelemetry(); // Clean telemetry to make it ready for next build submission. _buildTelemetry = null; @@ -1176,18 +1200,18 @@ void SerializeCaches() } } - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads of System.Diagnostics.DiagnosticSource, TODO: when this is agreed to perf-wise enable instrumenting using activities anywhere... + [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - OpenTelemetryManager.Instance.DefaultActivitySource? - .StartActivity("Build")? - .WithTags(_buildTelemetry) - .WithTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( - includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)) - .WithStartTime(_buildTelemetry!.InnerStartAt) - .Dispose(); - OpenTelemetryManager.Instance.ForceFlush(); + TelemetryManager.Instance.Initialize(isStandalone: false); + + using IActivity? activity = TelemetryManager.Instance + ?.DefaultActivitySource + ?.StartActivity(TelemetryConstants.Build) + ?.SetTags(_buildTelemetry) + ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, + includeTargetDetails: false)); } /// @@ -3040,8 +3064,7 @@ private ILoggingService CreateLoggingService( loggerSwitchParameters: null, verbosity: LoggerVerbosity.Quiet); - _telemetryConsumingLogger = - new InternalTelemetryConsumingLogger(); + _telemetryConsumingLogger = new InternalTelemetryConsumingLogger(); ForwardingLoggerRecord[] forwardingLogger = { new ForwardingLoggerRecord(_telemetryConsumingLogger, forwardingLoggerDescription) }; @@ -3053,7 +3076,6 @@ private ILoggingService CreateLoggingService( loggingService.EnableTargetOutputLogging = true; } - try { if (loggers != null) @@ -3166,7 +3188,20 @@ private static void LogDeferredMessages(ILoggingService loggingService, IEnumera foreach (var message in deferredBuildMessages) { - loggingService.LogCommentFromText(BuildEventContext.Invalid, message.Importance, message.Text); + if (message.MessageSeverity is DeferredBuildMessageSeverity.Warning) + { + loggingService.LogWarningFromText( + BuildEventContext.Invalid, + subcategoryResourceName: null, + warningCode: message.Code, + helpKeyword: null, + file: BuildEventFileInfo.Empty, + message: message.Text); + } + else + { + loggingService.LogCommentFromText(BuildEventContext.Invalid, message.Importance, message.Text); + } // If message includes a file path, include that file if (message.FilePath is not null) @@ -3265,6 +3300,8 @@ private void Dispose(bool disposing) s_singletonInstance = null; } + TelemetryManager.Instance?.Dispose(); + _disposed = true; } } diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 9bd05788271..e1df9945e91 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -48,11 +48,7 @@ public sealed class MSBuildClient /// The command line to process. /// The first argument on the command line is assumed to be the name/path of the executable, and is ignored. /// -#if FEATURE_GET_COMMANDLINE - private readonly string _commandLine; -#else private readonly string[] _commandLine; -#endif /// /// The MSBuild client execution result. @@ -112,13 +108,7 @@ public sealed class MSBuildClient /// on the command line is assumed to be the name/path of the executable, and is ignored /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. - public MSBuildClient( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation) + public MSBuildClient(string[] commandLine, string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); @@ -162,12 +152,7 @@ private void CreateNodePipeStream() public MSBuildClientExitResult Execute(CancellationToken cancellationToken) { // Command line in one string used only in human readable content. - string descriptiveCommandLine = -#if FEATURE_GET_COMMANDLINE - _commandLine; -#else - string.Join(" ", _commandLine); -#endif + string descriptiveCommandLine = string.Join(" ", _commandLine); CommunicationsUtilities.Trace("Executing build with command line '{0}'", descriptiveCommandLine); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index e5eecc0ef06..c29d76b1c52 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -161,19 +161,19 @@ protected void ShutdownAllNodes(bool nodeReuse, NodeContextTerminateDelegate ter int timeout = 30; // Attempt to connect to the process with the handshake without low priority. - Stream nodeStream = TryConnectToProcess(nodeProcess.Id, timeout, NodeProviderOutOfProc.GetHandshake(nodeReuse, false)); + Stream nodeStream = TryConnectToProcess(nodeProcess.Id, timeout, NodeProviderOutOfProc.GetHandshake(nodeReuse, false), out HandshakeResult result); if (nodeStream == null) { // If we couldn't connect attempt to connect to the process with the handshake including low priority. - nodeStream = TryConnectToProcess(nodeProcess.Id, timeout, NodeProviderOutOfProc.GetHandshake(nodeReuse, true)); + nodeStream = TryConnectToProcess(nodeProcess.Id, timeout, NodeProviderOutOfProc.GetHandshake(nodeReuse, true), out result); } if (nodeStream != null) { // If we're able to connect to such a process, send a packet requesting its termination CommunicationsUtilities.Trace("Shutting down node with pid = {0}", nodeProcess.Id); - NodeContext nodeContext = new NodeContext(0, nodeProcess, nodeStream, factory, terminateNode); + NodeContext nodeContext = new NodeContext(0, nodeProcess, nodeStream, factory, terminateNode, result.NegotiatedPacketVersion); nodeContext.SendData(new NodeBuildComplete(false /* no node reuse */)); nodeStream.Dispose(); } @@ -216,6 +216,7 @@ protected IList GetNodes( } } + bool nodeReuseRequested = Handshake.IsHandshakeOptionEnabled(hostHandshake.HandshakeOptions, HandshakeOptions.NodeReuse); // Get all process of possible running node processes for reuse and put them into ConcurrentQueue. // Processes from this queue will be concurrently consumed by TryReusePossibleRunningNodes while // trying to connect to them and reuse them. When queue is empty, no process to reuse left @@ -224,7 +225,7 @@ protected IList GetNodes( ConcurrentQueue possibleRunningNodes = null; #if FEATURE_NODE_REUSE // Try to connect to idle nodes if node reuse is enabled. - if (_componentHost.BuildParameters.EnableNodeReuse) + if (nodeReuseRequested) { IList possibleRunningNodesList; (expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation); @@ -236,6 +237,7 @@ protected IList GetNodes( } } #endif + ConcurrentQueue nodeContexts = new(); ConcurrentQueue exceptions = new(); int currentProcessId = EnvironmentUtilities.CurrentProcessId; @@ -243,7 +245,12 @@ protected IList GetNodes( { try { - if (!TryReuseAnyFromPossibleRunningNodes(currentProcessId, nodeId) && !StartNewNode(nodeId)) + if (nodeReuseRequested && TryReuseAnyFromPossibleRunningNodes(currentProcessId, nodeId)) + { + return; + } + + if (!StartNewNode(nodeId)) { // We were unable to reuse or launch a node. CommunicationsUtilities.Trace("FAILED TO CONNECT TO A CHILD NODE"); @@ -283,7 +290,7 @@ bool TryReuseAnyFromPossibleRunningNodes(int currentProcessId, int nodeId) _processesToIgnore.TryAdd(nodeLookupKey, default); // Attempt to connect to each process in turn. - Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, 0 /* poll, don't wait for connections */, hostHandshake); + Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, 0 /* poll, don't wait for connections */, hostHandshake, out HandshakeResult result); if (nodeStream != null) { // Connection successful, use this node. @@ -294,7 +301,7 @@ bool TryReuseAnyFromPossibleRunningNodes(int currentProcessId, int nodeId) BuildEventContext = new BuildEventContext(nodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId) }); - CreateNodeContext(nodeId, nodeToReuse, nodeStream); + CreateNodeContext(nodeId, nodeToReuse, nodeStream, result.NegotiatedPacketVersion); return true; } } @@ -344,13 +351,13 @@ bool StartNewNode(int nodeId) // to the debugger process. Instead, use MSBUILDDEBUGONSTART=1 // Now try to connect to it. - Stream nodeStream = TryConnectToProcess(msbuildProcess.Id, TimeoutForNewNodeCreation, hostHandshake); + Stream nodeStream = TryConnectToProcess(msbuildProcess.Id, TimeoutForNewNodeCreation, hostHandshake, out HandshakeResult result); if (nodeStream != null) { // Connection successful, use this node. CommunicationsUtilities.Trace("Successfully connected to created node {0} which is PID {1}", nodeId, msbuildProcess.Id); - CreateNodeContext(nodeId, msbuildProcess, nodeStream); + CreateNodeContext(nodeId, msbuildProcess, nodeStream, result.NegotiatedPacketVersion); return true; } @@ -379,9 +386,9 @@ bool StartNewNode(int nodeId) return false; } - void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream) + void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte negotiatedVersion) { - NodeContext nodeContext = new(nodeId, nodeToReuse, nodeStream, factory, terminateNode, hostHandshake.HandshakeOptions); + NodeContext nodeContext = new(nodeId, nodeToReuse, nodeStream, factory, terminateNode, negotiatedVersion, hostHandshake.HandshakeOptions); nodeContexts.Enqueue(nodeContext); createNode(nodeContext); } @@ -447,7 +454,7 @@ private static void ValidateRemotePipeSecurityOnWindows(NamedPipeClientStream no /// /// Attempts to connect to the specified process. /// - private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake handshake) + private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake handshake, out HandshakeResult result) { // Try and connect to the process. string pipeName = NamedPipeUtil.GetPlatformSpecificPipeName(nodeProcessId); @@ -467,7 +474,7 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han try { - if (TryConnectToPipeStream(nodeStream, pipeName, handshake, timeout, out HandshakeResult result)) + if (TryConnectToPipeStream(nodeStream, pipeName, handshake, timeout, out result)) { return nodeStream; } @@ -491,6 +498,8 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han nodeStream?.Dispose(); } + result = HandshakeResult.Failure(HandshakeStatus.Undefined, "Check the COMM traces to diagnose the issue with communication."); + return null; } @@ -529,17 +538,16 @@ internal static bool TryConnectToPipeStream(NamedPipeClientStream nodeStream, st CommunicationsUtilities.Trace("Reading handshake from pipe {0}", pipeName); - if ( - - nodeStream.TryReadEndOfHandshakeSignal(true, + if (nodeStream.TryReadEndOfHandshakeSignal( + true, #if NETCOREAPP2_1_OR_GREATER - timeout, + timeout, #endif - out HandshakeResult innerResult)) + out HandshakeResult innerResult)) { // We got a connection. CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", pipeName); - result = HandshakeResult.Success(0); + result = HandshakeResult.Success(0, innerResult.NegotiatedPacketVersion); return true; } else @@ -643,6 +651,12 @@ private enum ExitPacketState /// private ExitPacketState _exitPacketState; + /// + /// The minimum packet version supported by both the host and the node. + /// + private readonly byte _negotiatedPacketVersion; + + #if FEATURE_APM // used in BodyReadComplete callback to avoid allocations due to passing state through BeginRead private int _currentPacketLength; @@ -657,6 +671,7 @@ public NodeContext( Stream nodePipe, INodePacketFactory factory, NodeContextTerminateDelegate terminateDelegate, + byte negotiatedVersion, HandshakeOptions handshakeOptions = HandshakeOptions.None) { _nodeId = nodeId; @@ -670,6 +685,7 @@ public NodeContext( _writeTranslator = BinaryTranslator.GetWriteTranslator(_writeBufferMemoryStream); _terminateDelegate = terminateDelegate; _handshakeOptions = handshakeOptions; + _negotiatedPacketVersion = negotiatedVersion; #if FEATURE_APM _headerReadCompleteCallback = HeaderReadComplete; _bodyReadCompleteCallback = BodyReadComplete; @@ -830,8 +846,8 @@ private void DrainPacketQueue(object state) if (extendedHeaderCreated) { // Write extended header with version BEFORE writing packet data - NodePacketTypeExtensions.WriteVersion(writeStream, NodePacketTypeExtensions.PacketVersion); - writeTranslator.PacketVersion = NodePacketTypeExtensions.PacketVersion; + NodePacketTypeExtensions.WriteVersion(writeStream, context._negotiatedPacketVersion); + writeTranslator.NegotiatedPacketVersion = context._negotiatedPacketVersion; } packet.Translate(writeTranslator); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index fc955807bfe..d0856898473 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -662,7 +662,7 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN HandshakeOptions hostContext = nodeKey.HandshakeOptions; // If runtime host path is null it means we don't have MSBuild.dll path resolved and there is no need to include it in the command line arguments. - string commandLineArgsPlaceholder = "\"{0}\" /nologo /nodemode:2 /nodereuse:{1} /low:{2} "; + string commandLineArgsPlaceholder = "\"{0}\" /nologo /nodemode:2 /nodereuse:{1} /low:{2} /parentpacketversion:{3} "; // Generate a unique node ID for communication purposes using atomic increment. int communicationNodeId = Interlocked.Increment(ref _nextNodeId); @@ -685,7 +685,7 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN // There is always one task host per host context so we always create just 1 one task host node here. nodeContexts = GetNodes( runtimeHostPath, - string.Format(commandLineArgsPlaceholder, Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName), NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority), + string.Format(commandLineArgsPlaceholder, Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName), NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority, NodePacketTypeExtensions.PacketVersion), communicationNodeId, this, handshake, @@ -709,7 +709,7 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN nodeContexts = GetNodes( msbuildLocation, - string.Format(commandLineArgsPlaceholder, string.Empty, NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority), + string.Format(commandLineArgsPlaceholder, string.Empty, NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority, NodePacketTypeExtensions.PacketVersion), communicationNodeId, this, new Handshake(hostContext), diff --git a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs index 63f9a7b7f29..d45e5bd870f 100644 --- a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs +++ b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs @@ -9,6 +9,7 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Experimental.FileAccess; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -44,7 +45,7 @@ public void InitializeComponent(IBuildComponentHost host) { _scheduler = host.GetComponent(BuildComponentType.Scheduler) as IScheduler; _configCache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; - _tempDirectory = FileUtilities.EnsureNoTrailingSlash(FileUtilities.TempFileDirectory); + _tempDirectory = FrameworkFileUtilities.EnsureNoTrailingSlash(FileUtilities.TempFileDirectory); } public void ShutdownComponent() diff --git a/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs b/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs new file mode 100644 index 00000000000..dc5c5d0d9ed --- /dev/null +++ b/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs @@ -0,0 +1,279 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework.Telemetry; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; + +#nullable enable + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Tracks and categorizes build errors for telemetry purposes. + /// + internal sealed class BuildErrorTelemetryTracker + { + // Use an enum internally for efficient tracking, convert to string only when needed + internal enum ErrorCategory + { + Compiler, + MSBuildGeneral, + MSBuildEvaluation, + MSBuildExecution, + MSBuildGraph, + Tasks, + SDKResolvers, + NETSDK, + NuGet, + BuildCheck, + NativeToolchain, + CodeAnalysis, + Razor, + WPF, + AspNet, + Other, + } + + /// + /// Error counts by category index (using enum ordinal). + /// + private readonly int[] _errorCounts = new int[Enum.GetValues(typeof(ErrorCategory)).Length]; + + /// + /// Tracks the primary failure category (category with highest count). + /// + private ErrorCategory _primaryCategory; + + /// + /// Tracks the highest error count for primary category determination. + /// + private int _primaryCategoryCount; + + /// + /// Tracks an error for telemetry purposes by categorizing it. + /// + /// The error code from the BuildErrorEventArgs. + /// The subcategory from the BuildErrorEventArgs. + public void TrackError(string? errorCode, string? subcategory) + { + // Categorize the error + ErrorCategory category = CategorizeError(errorCode, subcategory); + int categoryIndex = (int)category; + + // Increment the count for this category using Interlocked for thread safety + int newCount = System.Threading.Interlocked.Increment(ref _errorCounts[categoryIndex]); + + // Update primary category if this one now has the highest count + // Use a simple compare-and-swap pattern for thread-safe update + int currentMax = System.Threading.Interlocked.CompareExchange(ref _primaryCategoryCount, 0, 0); + if (newCount > currentMax) + { + // Try to update both the count and category atomically + if (System.Threading.Interlocked.CompareExchange(ref _primaryCategoryCount, newCount, currentMax) == currentMax) + { + _primaryCategory = category; + } + } + } + + /// + /// Populates build telemetry with error categorization data. + /// + /// The BuildTelemetry object to populate with error data. + public void PopulateBuildTelemetry(BuildTelemetry buildTelemetry) + { + buildTelemetry.ErrorCounts = new ErrorCountsInfo( + Compiler: GetCountOrNull(ErrorCategory.Compiler), + MsBuildGeneral: GetCountOrNull(ErrorCategory.MSBuildGeneral), + MsBuildEvaluation: GetCountOrNull(ErrorCategory.MSBuildEvaluation), + MsBuildExecution: GetCountOrNull(ErrorCategory.MSBuildExecution), + MsBuildGraph: GetCountOrNull(ErrorCategory.MSBuildGraph), + Task: GetCountOrNull(ErrorCategory.Tasks), + SdkResolvers: GetCountOrNull(ErrorCategory.SDKResolvers), + NetSdk: GetCountOrNull(ErrorCategory.NETSDK), + NuGet: GetCountOrNull(ErrorCategory.NuGet), + BuildCheck: GetCountOrNull(ErrorCategory.BuildCheck), + NativeToolchain: GetCountOrNull(ErrorCategory.NativeToolchain), + CodeAnalysis: GetCountOrNull(ErrorCategory.CodeAnalysis), + Razor: GetCountOrNull(ErrorCategory.Razor), + Wpf: GetCountOrNull(ErrorCategory.WPF), + AspNet: GetCountOrNull(ErrorCategory.AspNet), + Other: GetCountOrNull(ErrorCategory.Other)); + + // Set the primary failure category + if (_primaryCategoryCount > 0) + { + buildTelemetry.FailureCategory = _primaryCategory.ToString(); + } + } + + /// + /// Gets the error count for a category, returning null if zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int? GetCountOrNull(ErrorCategory category) + { + int count = System.Threading.Interlocked.CompareExchange(ref _errorCounts[(int)category], 0, 0); + return count > 0 ? count : null; + } + + /// + /// Categorizes an error based on its error code and subcategory. + /// Uses a two-level character switch for O(1) prefix matching. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ErrorCategory CategorizeError(string? errorCode, string? subcategory) + { + if (string.IsNullOrEmpty(errorCode)) + { + return ErrorCategory.Other; + } + + // Check subcategory for compiler errors (CS*, VBC*, FS*) + if (!string.IsNullOrEmpty(subcategory) && IsCompilerPrefix(subcategory!)) + { + return ErrorCategory.Compiler; + } + + // Check error code patterns + if (IsCompilerPrefix(errorCode!)) + { + return ErrorCategory.Compiler; + } + + if (errorCode!.Length < 2) + { + return ErrorCategory.Other; + } + + // Two-level switch on first two characters for efficient prefix matching + char c0 = char.ToUpperInvariant(errorCode[0]); + char c1 = char.ToUpperInvariant(errorCode[1]); + + return (c0, c1) switch + { + // A* + ('A', 'S') when StartsWithASP(errorCode) => ErrorCategory.AspNet, + + // B* + ('B', 'C') => ErrorCategory.BuildCheck, + ('B', 'L') => ErrorCategory.AspNet, // Blazor + + // C* (careful: CS is handled by IsCompilerPrefix above) + ('C', 'A') => ErrorCategory.CodeAnalysis, + ('C', 'L') => ErrorCategory.NativeToolchain, + ('C', 'V') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'T' => ErrorCategory.NativeToolchain, // CVT* + ('C', >= '0' and <= '9') => ErrorCategory.NativeToolchain, // C1*, C2*, C4* (C/C++ compiler) + + // I* + ('I', 'D') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'E' => ErrorCategory.CodeAnalysis, // IDE* + + // L* + ('L', 'N') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'K' => ErrorCategory.NativeToolchain, // LNK* + + // M* + ('M', 'C') => ErrorCategory.WPF, // MC* (Markup Compiler) + ('M', 'S') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'B' => CategorizeMSBError(errorCode.AsSpan()), + ('M', 'T') => ErrorCategory.NativeToolchain, // MT* (Manifest Tool) + + // N* + ('N', 'E') when StartsWithNETSDK(errorCode) => ErrorCategory.NETSDK, + ('N', 'U') => ErrorCategory.NuGet, + + // R* + ('R', 'C') => ErrorCategory.NativeToolchain, // RC* (Resource Compiler) + ('R', 'Z') => ErrorCategory.Razor, + + // X* + ('X', 'C') => ErrorCategory.WPF, // XC* (XAML Compiler) + + _ => ErrorCategory.Other + }; + } + + /// + /// Checks if the error code starts with "ASP" (case-insensitive). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool StartsWithASP(string errorCode) + => errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'P'; + + /// + /// Checks if the error code starts with "NETSDK" (case-insensitive). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool StartsWithNETSDK(string errorCode) + => errorCode.Length >= 6 && + char.ToUpperInvariant(errorCode[2]) == 'T' && + char.ToUpperInvariant(errorCode[3]) == 'S' && + char.ToUpperInvariant(errorCode[4]) == 'D' && + char.ToUpperInvariant(errorCode[5]) == 'K'; + + /// + /// Checks if the string starts with a compiler error prefix (CS, FS, VBC). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsCompilerPrefix(string value) + { + if (value.Length < 2) + { + return false; + } + + char c0 = char.ToUpperInvariant(value[0]); + char c1 = char.ToUpperInvariant(value[1]); + + return (c0, c1) switch + { + ('C', 'S') => true, // CS* -> C# compiler + ('F', 'S') => true, // FS* -> F# compiler + ('V', 'B') => value.Length >= 3 && char.ToUpperInvariant(value[2]) == 'C', // VBC* -> VB compiler + _ => false + }; + } + + /// + /// Categorizes MSB error codes into granular MSBuild categories. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ErrorCategory CategorizeMSBError(ReadOnlySpan codeSpan) + { + // MSB error codes: 3-letter prefix + 4-digit number (e.g., MSB3026) + if (codeSpan.Length < 7 || !TryParseErrorNumber(codeSpan, out int errorNumber)) + { + return ErrorCategory.Other; + } + + return errorNumber switch + { + >= 3001 and <= 3999 => ErrorCategory.Tasks, + >= 4001 and <= 4099 => ErrorCategory.MSBuildGeneral, + >= 4100 and <= 4199 => ErrorCategory.MSBuildEvaluation, + >= 4200 and <= 4299 => ErrorCategory.SDKResolvers, + >= 4300 and <= 4399 => ErrorCategory.MSBuildExecution, + >= 4400 and <= 4499 => ErrorCategory.MSBuildGraph, + >= 4500 and <= 4999 => ErrorCategory.MSBuildGeneral, + >= 5001 and <= 5999 => ErrorCategory.MSBuildExecution, + >= 6001 and <= 6999 => ErrorCategory.MSBuildExecution, + _ => ErrorCategory.Other + }; + } + + /// + /// Parses the 4-digit error number from an MSB error code span (e.g., "MSB3026" -> 3026). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseErrorNumber(ReadOnlySpan codeSpan, out int errorNumber) + { + // Extract digits after "MSB" prefix (positions 3-6) + ReadOnlySpan digits = codeSpan.Slice(3, 4); +#if NET + return int.TryParse(digits, out errorNumber); +#else + return int.TryParse(digits.ToString(), out errorNumber); +#endif + } + } +} diff --git a/src/Build/BackEnd/Components/Logging/ILoggingService.cs b/src/Build/BackEnd/Components/Logging/ILoggingService.cs index 9296eb45927..6621c2b740f 100644 --- a/src/Build/BackEnd/Components/Logging/ILoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/ILoggingService.cs @@ -283,6 +283,12 @@ MessageImportance MinimumRequiredMessageImportance /// true if the build submission logged an errors, otherwise false. bool HasBuildSubmissionLoggedErrors(int submissionId); + /// + /// Populates build telemetry with error categorization data. + /// + /// The BuildTelemetry object to populate with error data. + void PopulateBuildTelemetryWithErrors(Framework.Telemetry.BuildTelemetry buildTelemetry); + /// /// Get the warnings that will be promoted to errors for the specified context. /// diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 487c10b69b0..24d8dcb0944 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -221,6 +221,11 @@ internal partial class LoggingService : ILoggingService, INodePacketHandler /// private readonly ISet _buildSubmissionIdsThatHaveLoggedBuildcheckErrors = new HashSet(); + /// + /// Tracker for build error telemetry. + /// + private readonly BuildErrorTelemetryTracker _errorTelemetryTracker = new BuildErrorTelemetryTracker(); + /// /// A list of warnings to treat as errors for an associated . If an empty set, all warnings are treated as errors. /// @@ -656,6 +661,15 @@ public bool HasBuildSubmissionLoggedErrors(int submissionId) return _buildSubmissionIdsThatHaveLoggedErrors?.Contains(submissionId) == true; } + /// + /// Populates build telemetry with error categorization data. + /// + /// The BuildTelemetry object to populate with error data. + public void PopulateBuildTelemetryWithErrors(Framework.Telemetry.BuildTelemetry buildTelemetry) + { + _errorTelemetryTracker.PopulateBuildTelemetry(buildTelemetry); + } + /// /// Returns a collection of warnings to be logged as errors for the specified build context. /// @@ -1656,6 +1670,8 @@ private void RouteBuildEvent(object loggingEvent) // Keep track of build submissions that have logged errors. If there is no build context, add BuildEventContext.InvalidSubmissionId. _buildSubmissionIdsThatHaveLoggedErrors.Add(submissionId); } + + _errorTelemetryTracker.TrackError(errorEvent.Code, errorEvent.Subcategory); } // Respect warning-promotion properties from the remote project @@ -1848,6 +1864,11 @@ private void UpdateMinimumMessageImportance(ILogger logger) // The null logger has no effect on minimum verbosity. Execution.BuildManager.NullLogger => null, + // Telemetry loggers only consume WorkerNodeTelemetryLogged events, not message events. + // They have no effect on minimum message verbosity. + TelemetryInfra.InternalTelemetryConsumingLogger => null, + Framework.Telemetry.InternalTelemetryForwardingLogger => null, + TerminalLogger terminalLogger => terminalLogger.GetMinimumMessageImportance(), _ => innerLogger.GetType().FullName == "Microsoft.Build.Logging.TerminalLogger" diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 89984c15b9d..37bbc129150 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -99,19 +99,16 @@ public void TrackTaskSubclassing(Type taskType, bool isMicrosoftOwned) // Check if this base type is a Microsoft-owned task // We identify Microsoft tasks by checking if they're in the Microsoft.Build namespace string? baseTypeName = baseType.FullName; - if (!string.IsNullOrEmpty(baseTypeName) && - (baseTypeName.StartsWith("Microsoft.Build.Tasks.") || + if (!string.IsNullOrEmpty(baseTypeName) && + (baseTypeName.StartsWith("Microsoft.Build.Tasks.") || baseTypeName.StartsWith("Microsoft.Build.Utilities."))) { // This is a subclass of a Microsoft-owned task // Track it only if it's NOT itself Microsoft-owned (i.e., user-authored subclass) if (!isMicrosoftOwned) { - if (!_msbuildTaskSubclassUsage.ContainsKey(baseTypeName)) - { - _msbuildTaskSubclassUsage[baseTypeName] = 0; - } - _msbuildTaskSubclassUsage[baseTypeName]++; + _msbuildTaskSubclassUsage.TryGetValue(baseTypeName, out int count); + _msbuildTaskSubclassUsage[baseTypeName] = count + 1; } // Stop at the first Microsoft-owned base class we find break; @@ -162,7 +159,7 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex Clean(); } } - + private void Clean() { _assemblyTaskFactoryTasksExecutedCount = 0; @@ -177,39 +174,24 @@ private void Clean() _msbuildTaskSubclassUsage.Clear(); } + private static void AddCountIfNonZero(Dictionary properties, string propertyName, int count) + { + if (count > 0) + { + properties[propertyName] = count.ToString(CultureInfo.InvariantCulture); + } + } + private Dictionary GetTaskFactoryProperties() { Dictionary properties = new(); - if (_assemblyTaskFactoryTasksExecutedCount > 0) - { - properties["AssemblyTaskFactoryTasksExecutedCount"] = _assemblyTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_intrinsicTaskFactoryTasksExecutedCount > 0) - { - properties["IntrinsicTaskFactoryTasksExecutedCount"] = _intrinsicTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_codeTaskFactoryTasksExecutedCount > 0) - { - properties["CodeTaskFactoryTasksExecutedCount"] = _codeTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_roslynCodeTaskFactoryTasksExecutedCount > 0) - { - properties["RoslynCodeTaskFactoryTasksExecutedCount"] = _roslynCodeTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_xamlTaskFactoryTasksExecutedCount > 0) - { - properties["XamlTaskFactoryTasksExecutedCount"] = _xamlTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_customTaskFactoryTasksExecutedCount > 0) - { - properties["CustomTaskFactoryTasksExecutedCount"] = _customTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } + AddCountIfNonZero(properties, "AssemblyTaskFactoryTasksExecutedCount", _assemblyTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "IntrinsicTaskFactoryTasksExecutedCount", _intrinsicTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "CodeTaskFactoryTasksExecutedCount", _codeTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "RoslynCodeTaskFactoryTasksExecutedCount", _roslynCodeTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "XamlTaskFactoryTasksExecutedCount", _xamlTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "CustomTaskFactoryTasksExecutedCount", _customTaskFactoryTasksExecutedCount); return properties; } @@ -217,23 +199,16 @@ private Dictionary GetTaskFactoryProperties() private Dictionary GetTaskProperties() { Dictionary properties = new(); - - var totalTasksExecuted = _assemblyTaskFactoryTasksExecutedCount + + + var totalTasksExecuted = _assemblyTaskFactoryTasksExecutedCount + _intrinsicTaskFactoryTasksExecutedCount + - _codeTaskFactoryTasksExecutedCount + + _codeTaskFactoryTasksExecutedCount + _roslynCodeTaskFactoryTasksExecutedCount + - _xamlTaskFactoryTasksExecutedCount + + _xamlTaskFactoryTasksExecutedCount + _customTaskFactoryTasksExecutedCount; - - if (totalTasksExecuted > 0) - { - properties["TasksExecutedCount"] = totalTasksExecuted.ToString(CultureInfo.InvariantCulture); - } - - if (_taskHostTasksExecutedCount > 0) - { - properties["TaskHostTasksExecutedCount"] = _taskHostTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } + + AddCountIfNonZero(properties, "TasksExecutedCount", totalTasksExecuted); + AddCountIfNonZero(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); return properties; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index d99df97edb7..2709e004fcb 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -1267,9 +1267,9 @@ private void UpdateStatisticsPostBuild() { ITelemetryForwarder telemetryForwarder = ((TelemetryForwarderProvider)_componentHost.GetComponent(BuildComponentType.TelemetryForwarder)) - .Instance; + ?.Instance; - if (!telemetryForwarder.IsTelemetryCollected) + if (telemetryForwarder == null || !telemetryForwarder.IsTelemetryCollected) { return; } @@ -1279,6 +1279,11 @@ private void UpdateStatisticsPostBuild() // Hence we need to fetch the original result from the cache - to get the data for all executed targets. BuildResult unfilteredResult = resultsCache.GetResultsForConfiguration(_requestEntry.Request.ConfigurationId); + if (unfilteredResult?.ResultsByTarget == null || _requestEntry.RequestConfiguration.Project?.Targets == null) + { + return; + } + foreach (var projectTargetInstance in _requestEntry.RequestConfiguration.Project.Targets) { bool wasExecuted = @@ -1287,6 +1292,11 @@ private void UpdateStatisticsPostBuild() // E.g. _SourceLinkHasSingleProvider can be brought explicitly via nuget (Microsoft.SourceLink.GitHub) as well as sdk projectTargetInstance.Value.Location.Equals(targetResult.TargetLocation); + // Get skip reason from TargetResult - it's set when targets are skipped for various reasons: + // - ConditionWasFalse: target's condition evaluated to false + // - PreviouslyBuiltSuccessfully/Unsuccessfully: target was already built in this session + TargetSkipReason skipReason = targetResult?.SkipReason ?? TargetSkipReason.None; + bool isFromNuget, isMetaprojTarget, isCustom; if (IsMetaprojTargetPath(projectTargetInstance.Value.FullPath)) @@ -1311,7 +1321,8 @@ private void UpdateStatisticsPostBuild() wasExecuted, isCustom, isMetaprojTarget, - isFromNuget); + isFromNuget, + skipReason); } TaskRegistry taskReg = _requestEntry.RequestConfiguration.Project.TaskRegistry; @@ -1326,12 +1337,15 @@ void CollectTasksStats(TaskRegistry taskRegistry) foreach (TaskRegistry.RegisteredTaskRecord registeredTaskRecord in taskRegistry.TaskRegistrations.Values.SelectMany(record => record)) { - telemetryForwarder.AddTask(registeredTaskRecord.TaskIdentity.Name, + telemetryForwarder.AddTask( + registeredTaskRecord.TaskIdentity.Name, registeredTaskRecord.Statistics.ExecutedTime, registeredTaskRecord.Statistics.ExecutedCount, registeredTaskRecord.Statistics.TotalMemoryConsumption, registeredTaskRecord.ComputeIfCustom(), - registeredTaskRecord.IsFromNugetCache); + registeredTaskRecord.IsFromNugetCache, + registeredTaskRecord.TaskFactoryAttributeName, + registeredTaskRecord.TaskFactoryParameters.Runtime); registeredTaskRecord.Statistics.Reset(); } @@ -1340,8 +1354,7 @@ void CollectTasksStats(TaskRegistry taskRegistry) } } - private static bool IsMetaprojTargetPath(string targetPath) - => targetPath.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase); + private static bool IsMetaprojTargetPath(string targetPath) => targetPath.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase); /// /// Saves the current operating environment (working directory and environment variables) diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 65b6903876a..d1b8b636f25 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -566,6 +566,10 @@ private bool CheckSkipTarget(ref bool stopProcessingStack, TargetEntry currentTa // If we've already dealt with this target and it didn't skip, let's log appropriately // Otherwise we don't want anything more to do with it. bool success = targetResult.ResultCode == TargetResultCode.Success; + + // Update the skip reason on the existing result for telemetry purposes + targetResult.SkipReason = success ? TargetSkipReason.PreviouslyBuiltSuccessfully : TargetSkipReason.PreviouslyBuiltUnsuccessfully; + var skippedTargetEventArgs = new TargetSkippedEventArgs(message: null) { BuildEventContext = _projectLoggingContext.BuildEventContext, @@ -574,7 +578,7 @@ private bool CheckSkipTarget(ref bool stopProcessingStack, TargetEntry currentTa ParentTarget = currentTargetEntry.ParentEntry?.Target.Name, BuildReason = currentTargetEntry.BuildReason, OriginallySucceeded = success, - SkipReason = success ? TargetSkipReason.PreviouslyBuiltSuccessfully : TargetSkipReason.PreviouslyBuiltUnsuccessfully, + SkipReason = targetResult.SkipReason, OriginalBuildEventContext = targetResult.OriginalBuildEventContext }; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs index 405d5421172..ea2914858f9 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs @@ -364,7 +364,8 @@ internal List GetDependencies(ProjectLoggingContext project _targetResult = new TargetResult( Array.Empty(), new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null), - projectLoggingContext.BuildEventContext); + projectLoggingContext.BuildEventContext, + TargetSkipReason.ConditionWasFalse); _state = TargetEntryState.Completed; if (projectLoggingContext.LoggingService.MinimumRequiredMessageImportance > MessageImportance.Low && diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs index 16afcef0c2e..56094d8f078 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs @@ -978,7 +978,7 @@ internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependenc // possibly the outputs list isn't actually the shortest list. However it always is the shortest // in the cases I've seen, and adding this optimization would make the code hard to read. - string oldestOutput = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(outputs[0].ToString())); + string oldestOutput = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(outputs[0].ToString())); ErrorUtilities.ThrowIfTypeDoesNotImplementToString(outputs[0]); DateTime oldestOutputFileTime = DateTime.MinValue; @@ -996,7 +996,7 @@ internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependenc if (oldestOutputFileTime == DateTime.MinValue) { // First output is missing: we must build the target - string arbitraryInput = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(inputs[0].ToString())); + string arbitraryInput = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(inputs[0].ToString())); ErrorUtilities.ThrowIfTypeDoesNotImplementToString(inputs[0]); dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(arbitraryInput, oldestOutput, null, null, OutofdateReason.MissingOutput); return true; @@ -1004,7 +1004,7 @@ internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependenc for (int i = 1; i < outputs.Count; i++) { - string candidateOutput = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(outputs[i].ToString())); + string candidateOutput = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(outputs[i].ToString())); ErrorUtilities.ThrowIfTypeDoesNotImplementToString(outputs[i]); DateTime candidateOutputFileTime = DateTime.MinValue; try @@ -1022,7 +1022,7 @@ internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependenc { // An output is missing: we must build the target string arbitraryInput = - EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(inputs[0].ToString())); + EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(inputs[0].ToString())); ErrorUtilities.ThrowIfTypeDoesNotImplementToString(inputs[0]); dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(arbitraryInput, candidateOutput, null, null, OutofdateReason.MissingOutput); return true; @@ -1039,7 +1039,7 @@ internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependenc // Now compare the oldest output with each input and break out if we find one newer. foreach (T input in inputs) { - string unescapedInput = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(input.ToString())); + string unescapedInput = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(input.ToString())); ErrorUtilities.ThrowIfTypeDoesNotImplementToString(input); DateTime inputFileTime = DateTime.MaxValue; try @@ -1127,8 +1127,8 @@ private void RecordUniqueInputsAndOutputs(IList inputs, IList outputs) /// true, if "input" is newer than "output" private bool IsOutOfDate(string input, string output, string inputItemName, string outputItemName) { - input = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(input)); - output = EscapingUtilities.UnescapeAll(FileUtilities.FixFilePath(output)); + input = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(input)); + output = EscapingUtilities.UnescapeAll(FrameworkFileUtilities.FixFilePath(output)); ProjectErrorUtilities.VerifyThrowInvalidProject(input.AsSpan().IndexOfAny(MSBuildConstants.InvalidPathChars) < 0, _project.ProjectFileLocation, "IllegalCharactersInFileOrDirectory", input, inputItemName); ProjectErrorUtilities.VerifyThrowInvalidProject(output.AsSpan().IndexOfAny(MSBuildConstants.InvalidPathChars) < 0, _project.ProjectFileLocation, "IllegalCharactersInFileOrDirectory", output, outputItemName); bool outOfDate = (CompareLastWriteTimes(input, output, out bool inputDoesNotExist, out bool outputDoesNotExist) == 1) || inputDoesNotExist; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 52de66f7ea2..51857855ddd 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -104,7 +104,7 @@ internal class TaskBuilder : ITaskBuilder, IBuildComponent private TargetLoggingContext _targetLoggingContext; /// - /// Full path to the project, for errors + /// Full path to the project. /// private string _projectFullPath; @@ -320,11 +320,24 @@ private async ValueTask ExecuteTask(TaskExecutionMode mode, Look if (_taskNode != null) { taskHost = new TaskHost(_componentHost, _buildRequestEntry, _targetChildInstance.Location, _targetBuilderCallback); - _taskExecutionHost.InitializeForTask(taskHost, _targetLoggingContext, _buildRequestEntry.RequestConfiguration.Project, _taskNode.Name, _taskNode.Location, _taskHostObject, _continueOnError != ContinueOnError.ErrorAndStop, + _taskExecutionHost.InitializeForTask( + taskHost, + _targetLoggingContext, + _buildRequestEntry.RequestConfiguration.Project, + _taskNode.Name, + _taskNode.Location, + _taskHostObject, + _continueOnError != ContinueOnError.ErrorAndStop, + _projectFullPath, #if FEATURE_APPDOMAIN taskHost.AppDomainSetup, #endif - taskHost.IsOutOfProc, _cancellationToken, _buildRequestEntry.TaskEnvironment); +#if !NET35 + _buildRequestEntry.Request.HostServices, +#endif + taskHost.IsOutOfProc, + _cancellationToken, + _buildRequestEntry.TaskEnvironment); } List taskParameterValues = CreateListOfParameterValues(); diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs index 3e319d90adf..fc01468bac7 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text.RegularExpressions; using System.Xml; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -79,7 +80,7 @@ internal static SdkResolverManifest Load(string filePath, string manifestFolder) { SdkResolverManifest manifest = ParseSdkResolverElement(reader, filePath); - manifest.Path = FileUtilities.FixFilePath(manifest.Path); + manifest.Path = FrameworkFileUtilities.FixFilePath(manifest.Path); if (!System.IO.Path.IsPathRooted(manifest.Path)) { manifest.Path = System.IO.Path.Combine(manifestFolder, manifest.Path); diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index e5b1e76f412..49ed1d610dd 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -25,12 +25,7 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// /// A callback used to execute command line build. /// - public delegate (int exitCode, string exitType) BuildCallback( -#if FEATURE_GET_COMMANDLINE - string commandLine); -#else - string[] commandLine); -#endif + public delegate (int exitCode, string exitType) BuildCallback(string[] commandLine); private readonly BuildCallback _buildFunction; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index fc5bca7e920..ab067c7d4ad 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -14,11 +14,7 @@ namespace Microsoft.Build.BackEnd /// internal sealed class ServerNodeBuildCommand : INodePacket { -#if FEATURE_GET_COMMANDLINE - private string _commandLine = default!; -#else private string[] _commandLine = default!; -#endif private string _startupDirectory = default!; private Dictionary _buildProcessEnvironment = default!; private CultureInfo _culture = default!; @@ -34,11 +30,7 @@ internal sealed class ServerNodeBuildCommand : INodePacket /// /// Command line including arguments /// -#if FEATURE_GET_COMMANDLINE - public string CommandLine => _commandLine; -#else public string[] CommandLine => _commandLine; -#endif /// /// The startup directory @@ -79,11 +71,7 @@ private ServerNodeBuildCommand() } public ServerNodeBuildCommand( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else string[] commandLine, -#endif string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, diff --git a/src/Build/BackEnd/Shared/TargetResult.cs b/src/Build/BackEnd/Shared/TargetResult.cs index 14ce318f869..efb94758c54 100644 --- a/src/Build/BackEnd/Shared/TargetResult.cs +++ b/src/Build/BackEnd/Shared/TargetResult.cs @@ -56,6 +56,11 @@ public class TargetResult : ITargetResult, ITranslatable /// private BuildEventContext _originalBuildEventContext; + /// + /// The reason why the target was skipped, if applicable. + /// + private TargetSkipReason _skipReason; + /// /// Initializes the results with specified items and result. /// @@ -68,13 +73,15 @@ public class TargetResult : ITargetResult, ITranslatable /// * in when Cancellation was requested /// * in ProjectCache.CacheResult.ConstructBuildResult /// - internal TargetResult(TaskItem[] items, WorkUnitResult result, BuildEventContext originalBuildEventContext = null) + /// The reason why the target was skipped, if applicable. + internal TargetResult(TaskItem[] items, WorkUnitResult result, BuildEventContext originalBuildEventContext = null, TargetSkipReason skipReason = TargetSkipReason.None) { ErrorUtilities.VerifyThrowArgumentNull(items); ErrorUtilities.VerifyThrowArgumentNull(result); _items = items; _result = result; _originalBuildEventContext = originalBuildEventContext; + _skipReason = skipReason; } /// @@ -149,6 +156,19 @@ public TargetResultCode ResultCode } } + /// + /// Gets the reason why the target was skipped, if applicable. + /// + /// The reason for skipping, or if the target was not skipped or the reason is unknown. + internal TargetSkipReason SkipReason + { + [DebuggerStepThrough] + get => _skipReason; + + [DebuggerStepThrough] + set => _skipReason = value; + } + public string TargetResultCodeToString() { switch (ResultCode) @@ -323,6 +343,7 @@ private void InternalTranslate(ITranslator translator) translator.Translate(ref _targetFailureDoesntCauseBuildFailure); translator.Translate(ref _afterTargetsHaveFailed); translator.TranslateOptionalBuildEventContext(ref _originalBuildEventContext); + translator.TranslateEnum(ref _skipReason, (int)_skipReason); TranslateItems(translator); } diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs index a15e93ca7a6..bec24b034f7 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Internal; @@ -41,7 +40,13 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { - return new AbsolutePath(Path.GetFullPath(path), ignoreRootedCheck: true); + // Opt-out for null path when Wave18_4 is disabled - return null as-is. + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null) + { + return new AbsolutePath(path!, path!, ignoreRootedCheck: true); + } + + return new AbsolutePath(path, basePath: ProjectDirectory); } /// diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs index b9ff6eb12ce..1bf9009054a 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs @@ -63,7 +63,7 @@ public AbsolutePath ProjectDirectory { _currentDirectory = value; // Keep the thread-static in sync for use by Expander and Modifiers during property/item expansion. - // This allows Path.GetFullPath and %(FullPath) to resolve relative paths correctly in multithreaded mode. + // This allows Path.GetFullPath and %(FullPath) functions used in project files to resolve relative paths correctly in multithreaded mode. FileUtilities.CurrentThreadWorkingDirectory = value.Value; } } @@ -71,6 +71,12 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { + // Opt-out for null path when Wave18_4 is disabled - return null as-is. + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null) + { + return new AbsolutePath(path!, path!, ignoreRootedCheck: true); + } + return new AbsolutePath(path, ProjectDirectory); } diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 4b9f5aa4a5f..670a3866842 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -108,6 +108,11 @@ internal class TaskExecutionHost : IDisposable /// private string _taskName; + /// + /// The project file path that runs the task. + /// + private string _projectFile; + /// /// The XML location of the task element. /// @@ -226,6 +231,10 @@ internal TaskFactoryWrapper _UNITTESTONLY_TaskFactoryWrapper set => _taskFactoryWrapper = value; } +#if !NET35 + private HostServices _hostServices; +#endif + #if FEATURE_APPDOMAIN /// /// App domain configuration. @@ -252,22 +261,39 @@ public virtual void Dispose() /// /// Initialize to run a specific task. /// - public void InitializeForTask(IBuildEngine2 buildEngine, TargetLoggingContext loggingContext, ProjectInstance projectInstance, string taskName, ElementLocation taskLocation, ITaskHost taskHost, bool continueOnError, + public void InitializeForTask( + IBuildEngine2 buildEngine, + TargetLoggingContext loggingContext, + ProjectInstance projectInstance, + string taskName, + ElementLocation taskLocation, + ITaskHost taskHost, + bool continueOnError, + string projectFile, #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, #endif - bool isOutOfProc, CancellationToken cancellationToken, TaskEnvironment taskEnvironment) +#if !NET35 + HostServices hostServices, +#endif + bool isOutOfProc, + CancellationToken cancellationToken, + TaskEnvironment taskEnvironment) { _buildEngine = buildEngine; _projectInstance = projectInstance; _targetLoggingContext = loggingContext; _taskName = taskName; + _projectFile = projectFile; _taskLocation = taskLocation; _cancellationTokenRegistration = cancellationToken.Register(Cancel); _taskHost = taskHost; _taskExecutionIdle.Set(); #if FEATURE_APPDOMAIN AppDomainSetup = appDomainSetup; +#endif +#if !NET35 + _hostServices = hostServices; #endif IsOutOfProc = isOutOfProc; TaskEnvironment = taskEnvironment; @@ -970,7 +996,15 @@ private ITask InstantiateTask(int scheduledNodeId, in TaskHostParameters taskIde { if (_taskFactoryWrapper.TaskFactory is AssemblyTaskFactory assemblyTaskFactory) { - task = assemblyTaskFactory.CreateTaskInstance(_taskLocation, _taskLoggingContext, _buildComponentHost, taskIdentityParameters, + task = assemblyTaskFactory.CreateTaskInstance( + _taskLocation, + _taskLoggingContext, + _buildComponentHost, + taskIdentityParameters, + _projectFile, +#if !NET35 + _hostServices, +#endif #if FEATURE_APPDOMAIN AppDomainSetup, #endif @@ -1814,8 +1848,12 @@ private ITask CreateTaskHostTaskForOutOfProcFactory( taskHostParameters, taskLoadedType, useSidecarTaskHost: true, + _projectFile, #if FEATURE_APPDOMAIN AppDomainSetup, +#endif +#if !NET35 + _hostServices, #endif scheduledNodeId, TaskEnvironment); diff --git a/src/Build/CompatibilitySuppressions.xml b/src/Build/CompatibilitySuppressions.xml index 0497d618a92..e1a53b3ea2e 100644 --- a/src/Build/CompatibilitySuppressions.xml +++ b/src/Build/CompatibilitySuppressions.xml @@ -1,3 +1,48 @@  - \ No newline at end of file + + + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + diff --git a/src/Build/Construction/ProjectImportElement.cs b/src/Build/Construction/ProjectImportElement.cs index 6bbc04a9e7d..852ab8e7d05 100644 --- a/src/Build/Construction/ProjectImportElement.cs +++ b/src/Build/Construction/ProjectImportElement.cs @@ -52,7 +52,7 @@ internal ProjectImportElement(XmlElementWithLocation xmlElement, ProjectRootElem /// public string Project { - get => FileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.project)); + get => FrameworkFileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.project)); set { ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.project); @@ -71,7 +71,7 @@ public string Project /// public string Sdk { - get => FileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.sdk)); + get => FrameworkFileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.sdk)); set { ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.sdk); diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index 57366d1349b..83a084eb685 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1256,7 +1256,7 @@ public ProjectTargetElement AddTarget(string name) /// public ProjectUsingTaskElement AddUsingTask(string name, string assemblyFile, string assemblyName) { - ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, FileUtilities.FixFilePath(assemblyFile), assemblyName); + ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, FrameworkFileUtilities.FixFilePath(assemblyFile), assemblyName); AppendChild(usingTask); return usingTask; diff --git a/src/Build/Construction/ProjectUsingTaskElement.cs b/src/Build/Construction/ProjectUsingTaskElement.cs index 10e310e5550..cecae44497d 100644 --- a/src/Build/Construction/ProjectUsingTaskElement.cs +++ b/src/Build/Construction/ProjectUsingTaskElement.cs @@ -3,7 +3,7 @@ using System; using System.Diagnostics; - +using Microsoft.Build.Framework; using Microsoft.Build.ObjectModelRemoting; using Microsoft.Build.Shared; @@ -48,14 +48,14 @@ private ProjectUsingTaskElement(XmlElementWithLocation xmlElement, ProjectRootEl /// public string AssemblyFile { - get => FileUtilities.FixFilePath( + get => FrameworkFileUtilities.FixFilePath( GetAttributeValue(XMakeAttributes.assemblyFile)); set { ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.assemblyName); ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(AssemblyName), "OM_EitherAttributeButNotBoth", ElementName, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName); - value = FileUtilities.FixFilePath(value); + value = FrameworkFileUtilities.FixFilePath(value); SetOrRemoveAttribute(XMakeAttributes.assemblyFile, value, "Set usingtask AssemblyFile {0}", value); } } @@ -249,7 +249,7 @@ internal static ProjectUsingTaskElement CreateDisconnected(string taskName, stri if (!String.IsNullOrEmpty(assemblyFile)) { - usingTask.AssemblyFile = FileUtilities.FixFilePath(assemblyFile); + usingTask.AssemblyFile = FrameworkFileUtilities.FixFilePath(assemblyFile); } else { diff --git a/src/Build/Construction/Solution/SolutionProjectGenerator.cs b/src/Build/Construction/Solution/SolutionProjectGenerator.cs index e51789bbb38..c40de4d8294 100644 --- a/src/Build/Construction/Solution/SolutionProjectGenerator.cs +++ b/src/Build/Construction/Solution/SolutionProjectGenerator.cs @@ -1308,7 +1308,7 @@ private string GetMetaprojectName(ProjectInSolution project) baseName = project.ProjectName; } - baseName = FileUtilities.EnsureNoTrailingSlash(baseName); + baseName = FrameworkFileUtilities.EnsureNoTrailingSlash(baseName); return GetMetaprojectName(baseName); } diff --git a/src/Build/Definition/Toolset.cs b/src/Build/Definition/Toolset.cs index 92c8f8320ed..13288066f2c 100644 --- a/src/Build/Definition/Toolset.cs +++ b/src/Build/Definition/Toolset.cs @@ -13,9 +13,8 @@ using Microsoft.Build.Collections; using Microsoft.Build.Construction; using Microsoft.Build.Execution; -#if NET using Microsoft.Build.Framework; -#endif + using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -367,7 +366,7 @@ private set // technically hurt anything, but it doesn't look nice.) string toolsPathToUse = value; - if (FileUtilities.EndsWithSlash(toolsPathToUse)) + if (FrameworkFileUtilities.EndsWithSlash(toolsPathToUse)) { string rootPath = Path.GetPathRoot(Path.GetFullPath(toolsPathToUse)); diff --git a/src/Build/Evaluation/Conditionals/FunctionCallExpressionNode.cs b/src/Build/Evaluation/Conditionals/FunctionCallExpressionNode.cs index 61769eb3da9..a28b263a3e6 100644 --- a/src/Build/Evaluation/Conditionals/FunctionCallExpressionNode.cs +++ b/src/Build/Evaluation/Conditionals/FunctionCallExpressionNode.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; @@ -119,7 +120,7 @@ private static string ExpandArgumentForScalarParameter(string function, GenericE // Fix path before expansion if (isFilePath) { - argument = FileUtilities.FixFilePath(argument); + argument = FrameworkFileUtilities.FixFilePath(argument); } IList items = state.ExpandIntoTaskItems(argument); @@ -153,7 +154,7 @@ private List ExpandArgumentAsFileList(GenericExpressionNode argumentNode // Fix path before expansion if (isFilePath) { - argument = FileUtilities.FixFilePath(argument); + argument = FrameworkFileUtilities.FixFilePath(argument); } IList expanded = state.ExpandIntoTaskItems(argument); diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index b40d997fb47..3cb76b64630 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -1713,7 +1713,7 @@ private static object ExpandMSBuildThisFileProperty(string propertyName, IElemen } else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectory, StringComparison.OrdinalIgnoreCase)) { - value = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File)); + value = FrameworkFileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File)); } else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectoryNoRoot, StringComparison.OrdinalIgnoreCase)) { @@ -4004,7 +4004,7 @@ internal object Execute(object objectInstance, IPropertyProvider properties, if (_receiverType == typeof(File) || _receiverType == typeof(Directory) || _receiverType == typeof(Path)) { - argumentValue = FileUtilities.FixFilePath(argumentValue); + argumentValue = FrameworkFileUtilities.FixFilePath(argumentValue); } args[n] = EscapingUtilities.UnescapeAll(argumentValue); diff --git a/src/Build/Evaluation/IntrinsicFunctions.cs b/src/Build/Evaluation/IntrinsicFunctions.cs index c464f1b2a05..f0417038f7b 100644 --- a/src/Build/Evaluation/IntrinsicFunctions.cs +++ b/src/Build/Evaluation/IntrinsicFunctions.cs @@ -525,7 +525,7 @@ internal static bool DoesTaskHostExist(string runtime, string architecture) /// The specified path with a trailing slash. internal static string EnsureTrailingSlash(string path) { - return FileUtilities.EnsureTrailingSlash(path); + return FrameworkFileUtilities.EnsureTrailingSlash(path); } /// diff --git a/src/Build/Instance/HostServices.cs b/src/Build/Instance/HostServices.cs index 4028cb95247..589c026a485 100644 --- a/src/Build/Instance/HostServices.cs +++ b/src/Build/Instance/HostServices.cs @@ -76,8 +76,7 @@ public ITaskHost GetHostObject(string projectFile, string targetName, string tas return null; } - var monikerNameOrITaskHost = - hostObjects.GetAnyMatchingMonikerNameOrITaskHost(targetName, taskName); + var monikerNameOrITaskHost = hostObjects.GetAnyMatchingMonikerNameOrITaskHost(targetName, taskName); if (monikerNameOrITaskHost == null) { @@ -471,7 +470,15 @@ internal MonikerNameOrITaskHost GetAnyMatchingMonikerNameOrITaskHost(string targ return hostObject; } - return null; + // For out-of-proc hosts, tasks might have a fully qualified name, + // while the host object registration might use a short name. + // We'll try to find a match using the short name. + string taskShortName = taskName.Split('.').LastOrDefault(); + + // If we got a short name and it's different from the original task name, try looking up with it. + return !taskName.Equals(taskShortName, StringComparison.OrdinalIgnoreCase) && _hostObjects.TryGetValue(new TargetTaskKey(targetName, taskShortName), out hostObject) + ? hostObject + : null; } /// diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index fe0cdee00e6..da0ac9603d0 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -1390,13 +1390,13 @@ public void AddSdkResolvedEnvironmentVariable(string name, string value) // If the property has already been set as an environment variable, we do not overwrite it. if (_environmentVariableProperties.Contains(name)) { - _loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableAlreadySet", name, value); + LogIfValueDiffers(_environmentVariableProperties, name, value, "SdkEnvironmentVariableAlreadySet"); return; } // If another SDK already set it, we do not overwrite it. else if (_sdkResolvedEnvironmentVariableProperties?.Contains(name) == true) { - _loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableAlreadySetBySdk", name, value); + LogIfValueDiffers(_sdkResolvedEnvironmentVariableProperties, name, value, "SdkEnvironmentVariableAlreadySetBySdk"); return; } @@ -1412,8 +1412,18 @@ public void AddSdkResolvedEnvironmentVariable(string name, string value) ((IEvaluatorData)this) .SetProperty(name, value, isGlobalProperty: false, mayBeReserved: false, loggingContext: _loggingContext, isEnvironmentVariable: true, isCommandLineProperty: false); } + } - _loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableSet", name, value); + /// + /// Helper method to log a message if the attempted value differs from the existing value. + /// + private void LogIfValueDiffers(PropertyDictionary propertyDictionary, string name, string attemptedValue, string messageResourceName) + { + ProjectPropertyInstance existingProperty = propertyDictionary.GetProperty(name); + if (existingProperty != null && !string.Equals(existingProperty.EvaluatedValue, attemptedValue, StringComparison.Ordinal)) + { + _loggingContext.LogComment(MessageImportance.Low, messageResourceName, name, attemptedValue, existingProperty.EvaluatedValue); + } } /// diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 696f135412f..2b14dc6b998 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -865,8 +865,8 @@ internal TaskItem( ErrorUtilities.VerifyThrowArgumentLength(includeEscaped); ErrorUtilities.VerifyThrowArgumentLength(includeBeforeWildcardExpansionEscaped); - _includeEscaped = FileUtilities.FixFilePath(includeEscaped); - _includeBeforeWildcardExpansionEscaped = FileUtilities.FixFilePath(includeBeforeWildcardExpansionEscaped); + _includeEscaped = FrameworkFileUtilities.FixFilePath(includeEscaped); + _includeBeforeWildcardExpansionEscaped = FrameworkFileUtilities.FixFilePath(includeBeforeWildcardExpansionEscaped); _directMetadata = (directMetadata == null || directMetadata.Count == 0) ? null : directMetadata; // If the metadata was all removed, toss the dictionary _itemDefinitions = itemDefinitions; _projectDirectory = projectDirectory; diff --git a/src/Build/Instance/RunningObjectTable.cs b/src/Build/Instance/RunningObjectTable.cs index 9458a457bef..5d4ac4d984b 100644 --- a/src/Build/Instance/RunningObjectTable.cs +++ b/src/Build/Instance/RunningObjectTable.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Runtime.InteropServices; diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index 9da4014e849..bef70623b73 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -317,6 +317,10 @@ internal ITask CreateTaskInstance( TaskLoggingContext taskLoggingContext, IBuildComponentHost buildComponentHost, in TaskHostParameters taskIdentityParameters, + string projectFile, +#if !NET35 + HostServices hostServices, +#endif #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, #endif @@ -361,10 +365,14 @@ internal ITask CreateTaskInstance( ErrorUtilities.VerifyThrowInternalNull(buildComponentHost); mergedParameters = UpdateTaskHostParameters(mergedParameters); - (mergedParameters, bool isNetRuntime) = AddNetHostParamsIfNeeded(mergedParameters, getProperty); + mergedParameters = AddNetHostParamsIfNeeded(mergedParameters, getProperty); - bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false) - || isNetRuntime; + // Sidecar here means that the task host is launched with /nodeReuse:true and doesn't terminate + // after the task execution. This improves performance for tasks that run multiple times in a build. + // If the task host factory is explicitly requested, do not act as a sidecar task host. + // This is important as customers use task host factories for short lived tasks to release + // potential locks. + bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false); TaskHostTask task = new( taskLocation, @@ -373,8 +381,12 @@ internal ITask CreateTaskInstance( mergedParameters, _loadedType, useSidecarTaskHost: useSidecarTaskHost, + projectFile, #if FEATURE_APPDOMAIN appDomainSetup, +#endif +#if !NET35 + hostServices, #endif scheduledNodeId, taskEnvironment: taskEnvironment); @@ -631,7 +643,7 @@ private static TaskHostParameters MergeTaskFactoryParameterSets( /// Adds the properties necessary for .NET task host instantiation if the runtime is .NET. /// Returns a new TaskHostParameters with .NET host parameters added, or the original if not needed. /// - private static (TaskHostParameters TaskHostParams, bool isNetRuntime) AddNetHostParamsIfNeeded( + private static TaskHostParameters AddNetHostParamsIfNeeded( in TaskHostParameters currentParams, Func getProperty) { @@ -639,7 +651,7 @@ private static (TaskHostParameters TaskHostParams, bool isNetRuntime) AddNetHost if (currentParams.Runtime == null || !currentParams.Runtime.Equals(XMakeAttributes.MSBuildRuntimeValues.net, StringComparison.OrdinalIgnoreCase)) { - return (currentParams, isNetRuntime: false); + return currentParams; } string dotnetHostPath = getProperty(Constants.DotnetHostPathEnvVarName)?.EvaluatedValue; @@ -647,17 +659,16 @@ private static (TaskHostParameters TaskHostParams, bool isNetRuntime) AddNetHost if (string.IsNullOrEmpty(dotnetHostPath) || string.IsNullOrEmpty(ridGraphPath)) { - return (currentParams, isNetRuntime: false); + return currentParams; } string msBuildAssemblyPath = Path.GetDirectoryName(ridGraphPath) ?? string.Empty; - return (new TaskHostParameters( + return new TaskHostParameters( runtime: currentParams.Runtime, architecture: currentParams.Architecture, dotnetHostPath: dotnetHostPath, - msBuildAssemblyPath: msBuildAssemblyPath), - isNetRuntime: true); + msBuildAssemblyPath: msBuildAssemblyPath); } /// diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 9bf09a47b11..d677e3a2412 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -9,6 +9,7 @@ using System.Threading; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -143,6 +144,15 @@ internal class TaskHostTask : IGeneratedTask, ICancelableTask, INodePacketFactor /// private bool _useSidecarTaskHost = false; +#if !NET35 + private readonly HostServices _hostServices; +#endif + + /// + /// The project file path that requests task execution. + /// + private string _projectFile; + /// /// The task environment for virtualized environment operations. /// @@ -158,8 +168,12 @@ public TaskHostTask( TaskHostParameters taskHostParameters, LoadedType taskType, bool useSidecarTaskHost, + string projectFile, #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, +#endif +#if !NET35 + HostServices hostServices, #endif int scheduledNodeId, TaskEnvironment taskEnvironment) @@ -176,6 +190,10 @@ public TaskHostTask( #if FEATURE_APPDOMAIN _appDomainSetup = appDomainSetup; #endif +#if !NET35 + _hostServices = hostServices; +#endif + _projectFile = projectFile; _taskHostParameters = taskHostParameters; _useSidecarTaskHost = useSidecarTaskHost; _taskEnvironment = taskEnvironment; @@ -314,6 +332,9 @@ public bool Execute() (IDictionary)_taskEnvironment.GetEnvironmentVariables(), _buildComponentHost.BuildParameters.Culture, _buildComponentHost.BuildParameters.UICulture, +#if !NET35 + _hostServices, +#endif #if FEATURE_APPDOMAIN _appDomainSetup, #endif @@ -323,6 +344,8 @@ public bool Execute() BuildEngine.ContinueOnError, _taskType.Type.FullName, taskLocation, + _taskLoggingContext?.TargetLoggingContext?.Target?.Name, + _projectFile, _buildComponentHost.BuildParameters.LogTaskInputs, _setParameters, new Dictionary(_buildComponentHost.BuildParameters.GlobalProperties), diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index 24d15831948..ccdc34b700f 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -353,7 +353,7 @@ private static void RegisterTasksFromUsingTaskElement // don't want paths from imported projects being interpreted relative to the main project file. try { - assemblyFile = FileUtilities.FixFilePath(assemblyFile); + assemblyFile = FrameworkFileUtilities.FixFilePath(assemblyFile); if (assemblyFile != null && !Path.IsPathRooted(assemblyFile)) { diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index 3d40ddeb7af..73031a02798 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; @@ -296,6 +297,17 @@ private static bool TryParsePathParameter(string parameter, out string filePath) internal string FilePath { get; private set; } + /// + /// Gets or sets additional output file paths. When set, the binlog will be copied to all these paths + /// after the build completes. The primary FilePath will be used as the temporary write location. + /// + /// + /// This property is intended for internal use by MSBuild command-line processing. + /// It should not be set by external code or logger implementations. + /// Use multiple logger instances with different Parameters instead. + /// + public IReadOnlyList AdditionalFilePaths { get; init; } + /// Gets or sets the verbosity level. /// /// The binary logger Verbosity is always maximum (Diagnostic). It tries to capture as much @@ -505,6 +517,15 @@ public void Shutdown() } + // Log additional file paths before closing stream (so they're recorded in the binlog) + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0 && stream != null) + { + foreach (var additionalPath in AdditionalFilePaths) + { + LogMessage("BinLogCopyDestination=" + additionalPath); + } + } + if (stream != null) { // It's hard to determine whether we're at the end of decoding GZipStream @@ -514,6 +535,37 @@ public void Shutdown() stream.Dispose(); stream = null; } + + // Copy the binlog file to additional destinations if specified + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0) + { + foreach (var additionalPath in AdditionalFilePaths) + { + try + { + string directory = Path.GetDirectoryName(additionalPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + File.Copy(FilePath, additionalPath, overwrite: true); + } + catch (Exception ex) + { + // Log the error but don't fail the build + // Note: We can't use LogMessage here since the stream is already closed + string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword( + out _, + out _, + "ErrorCopyingBinaryLog", + FilePath, + additionalPath, + ex.Message); + + Console.Error.WriteLine(message); + } + } + } } private void RawEvents_LogDataSliceReceived(BinaryLogRecordKind recordKind, Stream stream) @@ -635,6 +687,78 @@ private void ProcessParameters(out bool omitInitialInfo) } private bool TryInterpretPathParameter(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, GetUniqueStamp, out filePath); + } + + private string GetUniqueStamp() + => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + + private static string ExpandPathParameter(string parameters) + => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + + /// + /// Extracts the file path from binary logger parameters string. + /// This is a helper method for processing multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog" or "output.binlog;ProjectImports=None") + /// The resolved file path, or "msbuild.binlog" if no path is specified + public static string ExtractFilePathFromParameters(string parameters) + { + const string DefaultBinlogFileName = "msbuild" + BinlogFileExtension; + + if (string.IsNullOrEmpty(parameters)) + { + return Path.GetFullPath(DefaultBinlogFileName); + } + + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + string filePath = null; + + foreach (var parameter in paramParts) + { + if (TryInterpretPathParameterStatic(parameter, out string extractedPath)) + { + filePath = extractedPath; + break; + } + } + + if (filePath == null) + { + filePath = DefaultBinlogFileName; + } + + try + { + return Path.GetFullPath(filePath); + } + catch + { + // If path resolution fails, return the original path + return filePath; + } + } + + /// + /// Attempts to interpret a parameter string as a file path. + /// + /// The parameter to interpret (e.g., "LogFile=output.binlog" or "output.binlog") + /// The extracted file path if the parameter is a path, otherwise the original parameter + /// True if the parameter is a valid file path (ends with .binlog or contains wildcards), false otherwise + private static bool TryInterpretPathParameterStatic(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, () => ExpandPathParameter(string.Empty), out filePath); + } + + /// + /// Core logic for interpreting a parameter string as a file path. + /// + /// The parameter to interpret + /// Function to expand wildcard placeholders + /// The extracted file path + /// True if the parameter is a valid file path + private static bool TryInterpretPathParameterCore(string parameter, Func wildcardExpander, out string filePath) { bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase); @@ -654,7 +778,7 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return hasProperExtension; } - filePath = parameter.Replace("{}", GetUniqueStamp(), StringComparison.Ordinal); + filePath = parameter.Replace("{}", wildcardExpander(), StringComparison.Ordinal); if (!hasProperExtension) { @@ -663,10 +787,149 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return true; } - private string GetUniqueStamp() - => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + /// + /// Extracts the non-file-path parameters from binary logger parameters string. + /// This is used to compare configurations between multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog;ProjectImports=None") + /// A normalized string of non-path parameters, or empty string if only path parameters + public static string ExtractNonPathParameters(string parameters) + { + if (string.IsNullOrEmpty(parameters)) + { + return string.Empty; + } - private static string ExpandPathParameter(string parameters) - => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + var nonPathParams = new List(); + + foreach (var parameter in paramParts) + { + // Skip file path parameters + if (TryInterpretPathParameterStatic(parameter, out _)) + { + continue; + } + + // This is a configuration parameter (like ProjectImports=None, OmitInitialInfo, etc.) + nonPathParams.Add(parameter); + } + + // Sort for consistent comparison + nonPathParams.Sort(StringComparer.OrdinalIgnoreCase); + return string.Join(";", nonPathParams); + } + + /// + /// Result of processing multiple binary logger parameter sets. + /// + public readonly struct ProcessedBinaryLoggerParameters + { + /// + /// List of distinct parameter sets that need separate logger instances. + /// + public IReadOnlyList DistinctParameterSets { get; } + + /// + /// If true, all parameter sets have identical configurations (only file paths differ), + /// so a single logger can be used with file copying for additional paths. + /// + public bool AllConfigurationsIdentical { get; } + + /// + /// Additional file paths to copy the binlog to (only valid when AllConfigurationsIdentical is true). + /// + public IReadOnlyList AdditionalFilePaths { get; } + + /// + /// List of duplicate file paths that were filtered out. + /// + public IReadOnlyList DuplicateFilePaths { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// List of distinct parameter sets that need separate logger instances. + /// Whether all parameter sets have identical configurations. + /// Additional file paths to copy the binlog to. + /// List of duplicate file paths that were filtered out. + public ProcessedBinaryLoggerParameters( + IReadOnlyList distinctParameterSets, + bool allConfigurationsIdentical, + IReadOnlyList additionalFilePaths, + IReadOnlyList duplicateFilePaths) + { + DistinctParameterSets = distinctParameterSets; + AllConfigurationsIdentical = allConfigurationsIdentical; + AdditionalFilePaths = additionalFilePaths; + DuplicateFilePaths = duplicateFilePaths; + } + } + + /// + /// Processes multiple binary logger parameter sets and returns distinct paths and configuration info. + /// + /// Array of parameter strings from command line + /// Processed result with distinct parameter sets and configuration info + public static ProcessedBinaryLoggerParameters ProcessParameters(string[] binaryLoggerParameters) + { + var distinctParameterSets = new List(); + var additionalFilePaths = new List(); + var duplicateFilePaths = new List(); + bool allConfigurationsIdentical = true; + + if (binaryLoggerParameters == null || binaryLoggerParameters.Length == 0) + { + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + if (binaryLoggerParameters.Length == 1) + { + distinctParameterSets.Add(binaryLoggerParameters[0]); + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + string primaryArguments = binaryLoggerParameters[0]; + string primaryNonPathParams = ExtractNonPathParameters(primaryArguments); + + var distinctFilePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var extractedFilePaths = new List(); + + // Check if all parameter sets have the same non-path configuration + for (int i = 0; i < binaryLoggerParameters.Length; i++) + { + string currentParams = binaryLoggerParameters[i]; + string currentNonPathParams = ExtractNonPathParameters(currentParams); + string currentFilePath = ExtractFilePathFromParameters(currentParams); + + // Check if this is a duplicate file path + if (distinctFilePaths.Add(currentFilePath)) + { + if (!string.Equals(primaryNonPathParams, currentNonPathParams, StringComparison.OrdinalIgnoreCase)) + { + allConfigurationsIdentical = false; + } + distinctParameterSets.Add(currentParams); + extractedFilePaths.Add(currentFilePath); + } + else + { + // Track duplicate paths for logging + duplicateFilePaths.Add(currentFilePath); + } + } + + // If all configurations are identical, compute additional file paths for copying + // Use the pre-extracted paths to avoid redundant ExtractFilePathFromParameters calls + if (allConfigurationsIdentical && distinctParameterSets.Count > 1) + { + for (int i = 1; i < extractedFilePaths.Count; i++) + { + additionalFilePaths.Add(extractedFilePaths[i]); + } + } + + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } } } diff --git a/src/Build/Logging/FileLogger.cs b/src/Build/Logging/FileLogger.cs index a2d306a1341..60fca6f6868 100644 --- a/src/Build/Logging/FileLogger.cs +++ b/src/Build/Logging/FileLogger.cs @@ -198,7 +198,7 @@ private void ApplyFileLoggerParameter(string parameterName, string parameterValu switch (parameterName.ToUpperInvariant()) { case "LOGFILE": - _logFileName = FileUtilities.FixFilePath(parameterValue); + _logFileName = FrameworkFileUtilities.FixFilePath(parameterValue); break; case "APPEND": _append = true; diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index dffb6064a90..8965696dfa8 100644 --- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs +++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs @@ -754,18 +754,23 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) UpdateNodeStatus(buildEventContext, null); } - // Continue execution and add project summary to the static part of the Console only if verbosity is higher than Quiet. - if (Verbosity <= LoggerVerbosity.Quiet) - { - return; - } - ProjectContext c = new(buildEventContext); if (_projects.TryGetValue(c, out TerminalProjectInfo? project)) { project.Succeeded = e.Succeeded; project.Stopwatch.Stop(); + + // In quiet mode, only show projects with errors or warnings. + // In higher verbosity modes, show projects based on other criteria. + if (Verbosity == LoggerVerbosity.Quiet && !project.HasErrorsOrWarnings) + { + // Still need to update counts even if not displaying + _buildErrorsCount += project.ErrorCount; + _buildWarningsCount += project.WarningCount; + return; + } + lock (_lock) { Terminal.BeginUpdate(); @@ -809,6 +814,7 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) // If this was a notable project build, we print it as completed only if it's produced an output or warnings/error. // If this is a test project, print it always, so user can see either a success or failure, otherwise success is hidden // and it is hard to see if project finished, or did not run at all. + // In quiet mode, we show the project header if there are errors/warnings (already checked above). else if (project.OutputPath is not null || project.BuildMessages is not null || project.IsTestProject) { // Show project build complete and its output @@ -839,7 +845,7 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) _buildErrorsCount += project.ErrorCount; _buildWarningsCount += project.WarningCount; - if (_showNodesDisplay) + if (_showNodesDisplay && Verbosity > LoggerVerbosity.Quiet) { DisplayNodes(); } @@ -1300,27 +1306,24 @@ private void WarningRaised(object sender, BuildWarningEventArgs e) } if (buildEventContext is not null - && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project) - && Verbosity > LoggerVerbosity.Quiet) + && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project)) { // If the warning is not a 'global' auth provider message, but is immediate, we render it immediately // but we don't early return so that the project also tracks it. - if (IsImmediateWarning(e.Code)) + if (IsImmediateWarning(e.Code) && Verbosity > LoggerVerbosity.Quiet) { RenderImmediateMessage(FormatWarningMessage(e, Indentation)); } // This is the general case - _most_ warnings are not immediate, so we add them to the project summary // and display them in the per-project and final summary. + // In quiet mode, we still accumulate so they can be shown in project-grouped form later. project.AddBuildMessage(TerminalMessageSeverity.Warning, FormatWarningMessage(e, TripleIndentation)); } else { // It is necessary to display warning messages reported by MSBuild, - // even if it's not tracked in _projects collection or the verbosity is Quiet. - // The idea here (similar to the implementation in ErrorRaised) is that - // even in Quiet scenarios we need to show warnings/errors, even if not in - // full project-tree view + // even if it's not tracked in _projects collection. RenderImmediateMessage(FormatWarningMessage(e, Indentation)); _buildWarningsCount++; } @@ -1385,14 +1388,15 @@ private void ErrorRaised(object sender, BuildErrorEventArgs e) BuildEventContext? buildEventContext = e.BuildEventContext; if (buildEventContext is not null - && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project) - && Verbosity > LoggerVerbosity.Quiet) + && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project)) { + // Always accumulate errors in the project, even in quiet mode, so they can be shown + // in project-grouped form later. project.AddBuildMessage(TerminalMessageSeverity.Error, FormatErrorMessage(e, TripleIndentation)); } else { - // It is necessary to display error messages reported by MSBuild, even if it's not tracked in _projects collection or the verbosity is Quiet. + // It is necessary to display error messages reported by MSBuild, even if it's not tracked in _projects collection. // For nicer formatting, any messages from the engine we strip the file portion from. bool hasMSBuildPlaceholderLocation = e.File.Equals("MSBUILD", StringComparison.Ordinal); RenderImmediateMessage(FormatErrorMessage(e, Indentation, requireFileAndLinePortion: !hasMSBuildPlaceholderLocation)); diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index d53ecf9d743..9f0c2b12145 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -38,14 +38,36 @@ - - + + + + + + SourcePackages\Collections\%(Link) + + + SourcePackages\Collections\%(Link) + + + + + + + + + SourcePackages\PooledObjects\%(Link) + + + SourcePackages\PooledObjects\%(Link) + + + @@ -182,7 +204,6 @@ - @@ -223,6 +244,7 @@ + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 68b59de6ee8..1ea24d63df3 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1384,14 +1384,12 @@ Errors: {3} {StrBegin="MSB4238: "} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - - - An SDK attempted set the environment variable "{0}" to "{1}". + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + MSB4189: <{1}> is not a valid child of the <{0}> element. {StrBegin="MSB4189: "} @@ -2430,9 +2428,6 @@ Utilization: {0} Average Utilization: {1:###.0} succeeded: {0} {0} whole number - - Loading telemetry libraries failed with exception: {0}. - Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. @@ -2449,6 +2444,10 @@ Utilization: {0} Average Utilization: {1:###.0} The directory does not exist: {0}. .NET Runtime Task Host could not be instantiated. See https://aka.ms/nettaskhost for details on how to resolve this error. + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + The custom task '{0}' required a fallback to out-of-process execution because the UsingTask definition does not specify the correct Runtime and Architecture. This reduces build performance. Update the UsingTask element to explicitly specify Runtime and Architecture attributes (e.g., Runtime="CLR4" Architecture="x64") or use TaskFactory="TaskHostFactory". @@ -2459,7 +2458,7 @@ Utilization: {0} Average Utilization: {1:###.0} diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 86d6fc9c60c..9ab02afe3bc 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -489,6 +489,11 @@ Číst proměnnou prostředí {0} + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Nepodařilo se zkopírovat binární protokol z {0} do {1}. {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Při čtení vstupních souborů mezipaměti pro výsledky z cesty {0} byla zjištěna chyba: {1} @@ -652,11 +657,6 @@ Metoda {0} se nedá zavolat s kolekcí, která obsahuje prázdné cílové názvy nebo názvy null. - - Loading telemetry libraries failed with exception: {0}. - Načítání knihoven telemetrie se nezdařilo s výjimkou: {0}. - - Output Property: Výstupní vlastnost: @@ -907,18 +907,13 @@ Chyby: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Sada SDK se pokusila nastavit proměnnou prostředí {0} na {1}, která ale již byla nastavena jako proměnná prostředí. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Sada SDK se pokusila nastavit proměnnou prostředí {0} na {1}, která ale již byla nastavena na „{2}“ jako proměnná prostředí. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Sada SDK se pokusila nastavit proměnnou prostředí {0} na {1}, která ale už byla nastavena jinou sadou SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Sada SDK se pokusila nastavit proměnnou prostředí {0} na {1}. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Sada SDK se pokusila nastavit proměnnou prostředí „{0}“ na „{1}“, která ale už byla nastavena jinou sadou SDK na „{2}“. diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 4adaf0b3973..5ce46079b62 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -489,6 +489,11 @@ Umgebungsvariable "{0}" lesen + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Fehler beim Kopieren des Binärprotokolls von „{0}“ nach „{1}“. {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Beim Lesen der Cachedateien für Eingabeergebnisse aus dem Pfad "{0}" wurde ein Fehler festgestellt: {1} @@ -652,11 +657,6 @@ Die Methode "{0}" kann nicht mit einer Sammlung aufgerufen werden, die NULL oder leere Zielnamen enthält. - - Loading telemetry libraries failed with exception: {0}. - Fehler beim Laden von Telemetriebibliotheken. Ausnahme:{0}. - - Output Property: Ausgabeeigenschaft: @@ -907,18 +907,13 @@ Fehler: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Ein SDK hat versucht, die Umgebungsvariable „{0}“ auf „{1}“ festzulegen, sie wurde aber bereits als Umgebungsvariable festgelegt. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Ein SDK hat versucht, die Umgebungsvariable „{0}“ auf „{1}“ festzulegen, sie wurde aber bereits als Umgebungsvariable auf „{2}“ festgelegt. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Ein SDK hat versucht, die Umgebungsvariable „{0}“ auf „{1}“ festzulegen, sie wurde aber bereits von einem anderen SDK festgelegt. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Ein SDK hat versucht, die Umgebungsvariable „{0}“ auf „{1}“ festzulegen. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Ein SDK hat versucht, die Umgebungsvariable „{0}“ auf „{1}“ festzulegen, sie wurde aber bereits von einem anderen SDK auf „{2}“ festgelegt. diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index ed841430d86..a72490d50a5 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -489,6 +489,11 @@ Leer la variable de entorno "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: no se pudo copiar el registro binario de "{0}" a "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Error al leer los archivos de caché de resultados de entrada en la ruta de acceso "{0}": {1} @@ -652,11 +657,6 @@ No se puede llamar al método {0} con una colección que contiene nombres de destino nulos o vacíos. - - Loading telemetry libraries failed with exception: {0}. - Error al cargar las bibliotecas de telemetría con la excepción: {0}. - - Output Property: Propiedad de salida: @@ -907,18 +907,13 @@ Errores: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Un SDK intentó establecer la variable de entorno "{0}" en "{1}", pero ya se estableció como variable de entorno. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Un SDK intentó establecer la variable de entorno "{0}" en "{1}", pero ya estaba establecida en "{2}" como variable de entorno. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Un SDK intentó establecer la variable de entorno "{0}" en "{1}", pero ya la estableció otro SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Un SDK intentó establecer la variable de entorno "{0}" en "{1}". + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Un SDK intentó establecer la variable de entorno "{0}" en "{1}", pero otro SDK ya la estableció en "{2}". diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 2a866b00d4e..9264bfb3118 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -489,6 +489,11 @@ Lire la variable d'environnement "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Échec de la copie du journal binaire de « {0} » vers « {1} ». {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: La lecture des fichiers cache des résultats d'entrée à partir du chemin "{0}" a rencontré une erreur : {1} @@ -652,11 +657,6 @@ Impossible d'appeler la méthode {0} avec une collection contenant des noms de cibles qui ont une valeur null ou qui sont vides. - - Loading telemetry libraries failed with exception: {0}. - Nous n’avons pas pu charger les bibliothèques de télémétrie avec l’exception : {0}. - - Output Property: Propriété de sortie : @@ -907,18 +907,13 @@ Erreurs : {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Un Kit de développement logiciel (SDK) a tenté de définir la variable d’environnement « {0} » à « {1} », alors qu'elle était déjà définie en tant que variable d'environnement. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Un Kit de développement logiciel (SDK) a tenté de définir la variable d’environnement « {0} » à « {1} », alors qu'elle était déjà définie sur « {2} » en tant que variable d'environnement. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Un Kit de développement logiciel (SDK) a tenté de définir la variable d’environnement « {0} » à « {1} », mais elle était déjà définie par un autre SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Un Kit de développement logiciel (SDK) a tenté de définir la variable d’environnement « {0} » à « {1} ». + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Un Kit de développement logiciel (SDK) a tenté de définir la variable d’environnement « {0} » à « {1} », mais elle était déjà définie sur « {2} » par un autre SDK. diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 99d4affd579..c3a339f9aba 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -489,6 +489,11 @@ Legge la variabile di ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: non è possibile copiare il log binario da "{0}" a "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: durante la lettura dei file della cache dei risultati di input dal percorso "{0}" è stato rilevato un errore: {1} @@ -652,11 +657,6 @@ Non è possibile chiamare il metodo {0} con una raccolta contenente nomi di destinazione Null o vuoti. - - Loading telemetry libraries failed with exception: {0}. - Caricamento delle librerie di telemetria non riuscito con eccezione: {0}. - - Output Property: Proprietà di output: @@ -907,18 +907,13 @@ Errori: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Un SDK ha tentato di impostare la variabile di ambiente "{0}" su "{1}" ma era già stata impostata come variabile di ambiente. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Un SDK ha tentato di impostare la variabile di ambiente "{0}" su "{1}" ma era già stata impostata su "{2}" come variabile di ambiente. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Un SDK ha tentato di impostare la variabile di ambiente "{0}" su "{1}" ma era già stata impostata da un altro SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Un SDK ha tentato di impostare la variabile di ambiente "{0}" su "{1}". + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Un SDK ha tentato di impostare la variabile di ambiente "{0}" su "{1}" ma era già stata impostata su "{2}" da un altro SDK. diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 38b60d181d3..bab87c64b11 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -489,6 +489,11 @@ 環境変数 "{0}" の読み取り + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: バイナリ ログを "{0}" から "{1}" にコピーできませんでした。{2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: パス "{0}" から入力結果キャッシュ ファイルを読み取る処理でエラーが発生しました: {1} @@ -652,11 +657,6 @@ Null または空のターゲット名を含むコレクションを指定してメソッド {0} を呼び出すことはできません。 - - Loading telemetry libraries failed with exception: {0}. - テレメトリ ライブラリの読み込みが次の例外で失敗しました: {0}。 - - Output Property: プロパティの出力: @@ -907,18 +907,13 @@ Errors: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - SDK は環境変数 "{0}" を "{1}" に設定しようとしましたが、それは既に環境変数として設定されていました。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + SDK は環境変数 "{0}" を "{1}" に設定しようとしましたが、それは既に環境変数として {2} に設定されていました。 - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - SDK は環境変数 "{0}" を "{1}" に設定しようとしましたが、それは既に別の SDK によって設定されていました。 - - - - An SDK attempted set the environment variable "{0}" to "{1}". - SDK が環境変数 "{0}" を "{1}" に設定しようとしました。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + SDK は環境変数 "{0}" を "{1}" に設定しようとしましたが、それは既に別の SDK によって {2} に設定されていました。 diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index e5aa1a02533..8fbaf0b2a79 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -489,6 +489,11 @@ 환경 변수 "{0}" 읽기 + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: 이진 로그를 "{0}"에서 "{1}"(으)로 복사하지 못했습니다. {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" 경로에서 입력 결과 캐시 파일을 읽는 중 오류가 발생했습니다. {1} @@ -652,11 +657,6 @@ null 또는 빈 대상 이름을 포함하는 컬렉션을 사용하여 {0} 메서드를 호출할 수 없습니다. - - Loading telemetry libraries failed with exception: {0}. - 예외 {0}(으)로 인해 원격 분석 라이브러리를 로드하지 못했습니다. - - Output Property: 출력 속성: @@ -907,18 +907,13 @@ Errors: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - SDK가 환경 변수 "{0}"을(를) "{1}"(으)로 설정하려고 했지만 이미 환경 변수로 설정되었습니다. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + SDK가 환경 변수 "{0}"을(를) "{1}"(으)로 설정하려고 했지만 이미 환경 변수로 "{2}"(으)로 설정되어 있었습니다. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - SDK가 환경 변수 "{0}"을(를) "{1}"(으)로 설정하려고 했지만 이미 다른 SDK에 의해 설정되었습니다. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - SDK가 환경 변수 "{0}"을(를) "{1}"(으)로 설정하려고 했습니다. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + SDK가 환경 변수 "{0}"을(를) "{1}"(으)로 설정하려고 했지만 이미 다른 SDK에 의해 "{2}"(으)로 설정되어 있었습니다. diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 866d90bcd9d..5468c688eba 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -489,6 +489,11 @@ Odczytaj zmienną środowiskową „{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: nie można skopiować dziennika binarnego z „{0}” do „{1}”. {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Podczas odczytywania plików wejściowej pamięci podręcznej wyników ze ścieżki „{0}” wystąpił błąd: {1} @@ -652,11 +657,6 @@ Metody {0} nie można wywołać przy użyciu kolekcji zawierającej nazwy docelowe o wartości null lub puste. - - Loading telemetry libraries failed with exception: {0}. - Ładowanie bibliotek telemetrii nie powiodło się. Wyjątek: {0}. - - Output Property: Właściwość danych wyjściowych: @@ -907,18 +907,13 @@ Błędy: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Zestaw SDK próbował ustawić zmienną środowiskową „{0}” na „{1}”, ale została już ustawiona jako zmienna środowiskowa. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Zestaw SDK próbował ustawić zmienną środowiskową „{0}” na „{1}”, ale została już ustawiona na wartość „{2}” jako zmienna środowiskowa. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Zestaw SDK próbował ustawić zmienną środowiskową „{0}” na „{1}”, ale została już ustawiona przez inny zestaw SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Zestaw SDK próbował ustawić zmienną środowiskową „{0}” na „{1}”. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Zestaw SDK próbował ustawić zmienną środowiskową „{0}” na „{1}”, ale została już ustawiona na wartość „{2}” przez inny zestaw SDK. diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index d27c50e5464..f7957c45cda 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -489,6 +489,11 @@ Ler a variável de ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: falha ao copiar o log binário de "{0}" para "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: a leitura dos arquivos de cache do resultado de entrada do caminho "{0}" encontrou um erro: {1} @@ -652,11 +657,6 @@ O método {0} não pode ser chamado com uma coleção que contém nomes de destino nulos ou vazios. - - Loading telemetry libraries failed with exception: {0}. - Falha ao carregar as bibliotecas de telemetria com a exceção: {0}. - - Output Property: Propriedade de Saída: @@ -907,18 +907,13 @@ Erros: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Um SDK tentou definir a variável de ambiente "{0}" como "{1}", mas ela já foi definida como variável de ambiente. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Um SDK tentou definir a variável de ambiente "{0}" como "{1}", mas ela já havia sido definida como "{2}" como uma variável de ambiente. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Um SDK tentou definir a variável de ambiente "{0}" como "{1}", mas ela já foi definida por outro SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Um SDK tentou definir a variável de ambiente "{0}" como "{1}". + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Um SDK tentou definir a variável de ambiente "{0}" como "{1}", mas ela já havia sido definida como "{2}" por outro SDK. diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 55d613e49cd..4b309e0cc21 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -489,6 +489,11 @@ Чтение переменной среды "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: не удалось скопировать двоичный журнал из "{0}" в "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: произошла ошибка при чтении входных файлов кэша результатов из пути "{0}": {1} @@ -652,11 +657,6 @@ Метод {0} не может быть вызван с коллекцией, содержащей целевые имена, которые пусты или равны NULL. - - Loading telemetry libraries failed with exception: {0}. - Не удалось загрузить библиотеки телеметрии с исключением: {0}. - - Output Property: Выходное свойство: @@ -907,18 +907,13 @@ Errors: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Пакет SDK попытался настроить для переменной среды "{0}" значение "{1}", но она уже была настроена в качестве переменной среды. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Пакет SDK попытался настроить для переменной среды "{0}" значение "{1}", но для нее уже было настроено значение "{2}" в качестве переменной среды. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Пакет SDK попытался настроить для переменной среды "{0}" значение "{1}", но она уже была настроена другим пакетом SDK. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Пакет SDK попытался настроить для переменной среды "{0}" значение "{1}". + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Пакет SDK попытался настроить для переменной среды "{0}" значение "{1}", но для нее уже было настроено значение "{2}" другим пакетом SDK. diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 7a5e481602b..87f9b73cded 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -489,6 +489,11 @@ "{0}" ortam değişkenini oku + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: İkili günlük dosyası "{0}" konumundan "{1}" konumuna kopyalanamadı. {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" yolundan giriş sonucu önbellek dosyaları okunurken bir hatayla karşılaşıldı: {1} @@ -652,11 +657,6 @@ {0} metosu null veya boş hedef adları içeren bir koleksiyonla çağrılamaz. - - Loading telemetry libraries failed with exception: {0}. - Telemetri kitaplıklarının yüklenmesi şu hayatla başarısız oldu: {0}. - - Output Property: Çıkış Özelliği: @@ -907,18 +907,13 @@ Hatalar: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - Bir SDK "{0}" ortam değişkenini "{1}" olarak ayarlamaya çalıştı ancak bu değişken zaten bir ortam değişkeni olarak ayarlanmıştı. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + Bir SDK "{0}" ortam değişkenini "{1}" olarak ayarlamaya çalıştı ancak bu değişken zaten bir ortam değişkeni olarak "{2}" değerine ayarlanmıştı. - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - Bir SDK "{0}" ortam değişkenini "{1}" olarak ayarlamaya çalıştı ancak bu değişken zaten başka bir SDK tarafından ayarlanmıştı. - - - - An SDK attempted set the environment variable "{0}" to "{1}". - Bir SDK, "{0}" ortam değişkenini "{1}" olarak ayarlamaya çalıştı. + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + Bir SDK "{0}" ortam değişkenini "{1}" olarak ayarlamaya çalıştı ancak bu değişken zaten başka bir SDK tarafından "{2}" olarak ayarlanmıştı. diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index d8da78803bc..ad7d1118c48 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -489,6 +489,11 @@ 读取环境变量“{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: 未能将二进制日志从“{0}”复制到“{1}”。{2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 从路径“{0}”读取输入结果缓存文件时遇到错误: {1} @@ -652,11 +657,6 @@ 无法使用包含 null 或空目标名称的集合调用方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 加载遥测库失败,出现异常: {0}。 - - Output Property: 输出属性: @@ -907,18 +907,13 @@ Errors: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - SDK 尝试将环境变量 "{0}" 设置为 "{1}",但它已设置为环境变量。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + SDK 尝试将环境变量 "{0}" 设置为 "{1}",但它已作为环境变量设置为 "{2}"。 - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - SDK 尝试将环境变量 "{0}" 设置为 "{1}",但已由另一个 SDK 设置。 - - - - An SDK attempted set the environment variable "{0}" to "{1}". - SDK 尝试将环境变量 "{0}" 设置为 "{1}"。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + SDK 尝试将环境变量 "{0}" 设置为 "{1}",但它已由另一个 SDK 设置为 "{2}"。 diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index d851b21ba23..6a78af31da7 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -489,6 +489,11 @@ 讀取環境變數 "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: 無法將二進位記錄從 "{0}" 複製到 "{1}"。{2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 從路徑 "{0}" 讀取輸入結果快取檔案發生錯誤: {1} @@ -652,11 +657,6 @@ 無法使用內含 null 或空白目標名稱的集合呼叫方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 載入遙測程式庫時發生例外狀況: {0}。 - - Output Property: 輸出屬性: @@ -907,18 +907,13 @@ Errors: {3} - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set as an environment variable. - 一個 SDK 嘗試將環境變數 "{0}" 設定為 "{1}",但該變數已設定為環境變數。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" as an environment variable. + SDK 嘗試將環境變數 "{0}" 設定為 "{1}",但該變數已設定為 "{2}" 做為環境變數。 - An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set by another SDK. - 一個 SDK 嘗試將環境變數 "{0}" 設定為 "{1}",但該變數已由另一個 SDK 設定。 - - - - An SDK attempted set the environment variable "{0}" to "{1}". - 一個 SDK 嘗試將環境變數 "{0}" 設定為 "{1}"。 + An SDK attempted to set the environment variable "{0}" to "{1}" but it was already set to "{2}" by another SDK. + SDK 嘗試將環境變數 "{0}" 設定為 "{1}",但該變數已由另一個 SDK 設定為 "{2}"。 diff --git a/src/Build/TelemetryInfra/ITelemetryForwarder.cs b/src/Build/TelemetryInfra/ITelemetryForwarder.cs index 15d021bfb81..916661a7aa2 100644 --- a/src/Build/TelemetryInfra/ITelemetryForwarder.cs +++ b/src/Build/TelemetryInfra/ITelemetryForwarder.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; namespace Microsoft.Build.TelemetryInfra; @@ -14,18 +15,26 @@ internal interface ITelemetryForwarder { bool IsTelemetryCollected { get; } - void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, - bool isFromNugetCache); + void AddTask( + string name, + TimeSpan cumulativeExecutionTime, + short executionsCount, + long totalMemoryConsumed, + bool isCustom, + bool isFromNugetCache, + string? taskFactoryName, + string? taskHostRuntime); /// /// Add info about target execution to the telemetry. /// - /// - /// Means anytime, not necessarily from the last time target was added to telemetry - /// - /// - /// - void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache); + /// The target name. + /// Whether the target was executed (not skipped). + /// Whether this is a custom target. + /// Whether the target is from a meta project. + /// Whether the target is from a NuGet package. + /// The reason the target was skipped, if applicable. + void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache, TargetSkipReason skipReason = TargetSkipReason.None); void FinalizeProcessing(LoggingContext loggingContext); } diff --git a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs index b028dd4b7fa..19feabfee3d 100644 --- a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs +++ b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs @@ -11,7 +11,9 @@ namespace Microsoft.Build.TelemetryInfra; internal sealed class InternalTelemetryConsumingLogger : ILogger { public LoggerVerbosity Verbosity { get; set; } + public string? Parameters { get; set; } + internal static event Action? TestOnly_InternalTelemetryAggregted; public void Initialize(IEventSource eventSource) @@ -51,6 +53,7 @@ private void FlushDataIntoConsoleIfRequested() { Console.WriteLine($"{target.Key} : {target.Value}"); } + Console.WriteLine("=========================================="); Console.WriteLine($"Tasks: ({_workerNodeTelemetryData.TasksExecutionData.Count})"); Console.WriteLine("Custom tasks:"); @@ -58,24 +61,28 @@ private void FlushDataIntoConsoleIfRequested() { Console.WriteLine($"{task.Key}"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by time:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.CumulativeExecutionTime)) { Console.WriteLine($"{task.Key} - {task.Value.CumulativeExecutionTime}"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by memory consumption:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.TotalMemoryBytes)) { Console.WriteLine($"{task.Key} - {task.Value.TotalMemoryBytes / 1024.0:0.00}kB"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by Executions count:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.ExecutionsCount)) { Console.WriteLine($"{task.Key} - {task.Value.ExecutionsCount}"); } + Console.WriteLine("=========================================="); } diff --git a/src/Build/TelemetryInfra/TelemetryDataUtils.cs b/src/Build/TelemetryInfra/TelemetryDataUtils.cs deleted file mode 100644 index e2759bec030..00000000000 --- a/src/Build/TelemetryInfra/TelemetryDataUtils.cs +++ /dev/null @@ -1,339 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Build.Framework.Telemetry -{ - internal static class TelemetryDataUtils - { - /// - /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. - /// - /// Data about tasks and target forwarded from nodes. - /// Controls whether Task details should attached to the telemetry. - /// Controls whether Target details should be attached to the telemetry. - /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . - public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails) - { - if (telemetryData == null) - { - return null; - } - - List telemetryItems = new(4); - - if (includeTasksDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Tasks, - JsonSerializer.Serialize(telemetryData.TasksExecutionData, _serializerOptions), false)); - } - - if (includeTargetDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Targets, - JsonSerializer.Serialize(telemetryData.TargetsExecutionData, _serializerOptions), false)); - } - - TargetsSummaryConverter targetsSummary = new(); - targetsSummary.Process(telemetryData.TargetsExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TargetsSummary, - JsonSerializer.Serialize(targetsSummary, _serializerOptions), false)); - - TasksSummaryConverter tasksSummary = new(); - tasksSummary.Process(telemetryData.TasksExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TasksSummary, - JsonSerializer.Serialize(tasksSummary, _serializerOptions), false)); - - return new NodeTelemetry(telemetryItems); - } - - private static JsonSerializerOptions _serializerOptions = CreateSerializerOptions(); - - private static JsonSerializerOptions CreateSerializerOptions() - { - var opt = new JsonSerializerOptions - { - Converters = - { - new TargetsDetailsConverter(), - new TasksDetailsConverter(), - new TargetsSummaryConverter(), - new TasksSummaryConverter(), - }, - }; - - return opt; - } - - private class TargetsDetailsConverter : JsonConverter?> - { - public override Dictionary? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - Dictionary? value, - JsonSerializerOptions options) - { - if (value == null) - { - throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data"); - } - - // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types - writer.WriteStartObject(); - - foreach (KeyValuePair valuePair in value) - { - string keyName = ShouldHashKey(valuePair.Key) ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; - - writer.WriteStartObject(keyName); - writer.WriteBoolean("WasExecuted", valuePair.Value); - writer.WriteBoolean(nameof(valuePair.Key.IsCustom), valuePair.Key.IsCustom); - writer.WriteBoolean(nameof(valuePair.Key.IsNuget), valuePair.Key.IsNuget); - writer.WriteBoolean(nameof(valuePair.Key.IsMetaProj), valuePair.Key.IsMetaProj); - writer.WriteEndObject(); - } - - writer.WriteEndObject(); - } - - private bool ShouldHashKey(TaskOrTargetTelemetryKey key) => key.IsCustom || key.IsMetaProj; - } - - private class TasksDetailsConverter : JsonConverter?> - { - public override Dictionary? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - Dictionary? value, - JsonSerializerOptions options) - { - if (value == null) - { - throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data"); - } - - // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types - writer.WriteStartObject(); - - foreach (KeyValuePair valuePair in value) - { - string keyName = valuePair.Key.IsCustom ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; - writer.WriteStartObject(keyName); - writer.WriteNumber(nameof(valuePair.Value.CumulativeExecutionTime.TotalMilliseconds), valuePair.Value.CumulativeExecutionTime.TotalMilliseconds); - writer.WriteNumber(nameof(valuePair.Value.ExecutionsCount), valuePair.Value.ExecutionsCount); - writer.WriteNumber(nameof(valuePair.Value.TotalMemoryBytes), valuePair.Value.TotalMemoryBytes); - writer.WriteBoolean(nameof(valuePair.Key.IsCustom), valuePair.Key.IsCustom); - writer.WriteBoolean(nameof(valuePair.Key.IsNuget), valuePair.Key.IsNuget); - writer.WriteEndObject(); - } - - writer.WriteEndObject(); - } - } - - private class TargetsSummaryConverter : JsonConverter - { - /// - /// Processes target execution data to compile summary statistics for both built-in and custom targets. - /// - /// Dictionary containing target execution data keyed by task identifiers. - public void Process(Dictionary targetsExecutionData) - { - foreach (KeyValuePair targetPair in targetsExecutionData) - { - TaskOrTargetTelemetryKey key = targetPair.Key; - bool wasExecuted = targetPair.Value; - - // Update loaded targets statistics (all targets are loaded) - UpdateTargetStatistics(key, isExecuted: false); - - // Update executed targets statistics (only targets that were actually executed) - if (wasExecuted) - { - UpdateTargetStatistics(key, isExecuted: true); - } - } - } - - private void UpdateTargetStatistics(TaskOrTargetTelemetryKey key, bool isExecuted) - { - // Select the appropriate target info collections based on execution state - TargetInfo builtinTargetInfo = isExecuted ? ExecutedBuiltinTargetInfo : LoadedBuiltinTargetInfo; - TargetInfo customTargetInfo = isExecuted ? ExecutedCustomTargetInfo : LoadedCustomTargetInfo; - - // Update either custom or builtin target info based on target type - TargetInfo targetInfo = key.IsCustom ? customTargetInfo : builtinTargetInfo; - - targetInfo.Total++; - if (key.IsNuget) - { - targetInfo.FromNuget++; - } - if (key.IsMetaProj) - { - targetInfo.FromMetaproj++; - } - } - - private TargetInfo LoadedBuiltinTargetInfo { get; } = new(); - private TargetInfo LoadedCustomTargetInfo { get; } = new(); - private TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); - private TargetInfo ExecutedCustomTargetInfo { get; } = new(); - - private class TargetInfo - { - public int Total { get; internal set; } - public int FromNuget { get; internal set; } - public int FromMetaproj { get; internal set; } - } - - public override TargetsSummaryConverter? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - TargetsSummaryConverter value, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteStartObject("Loaded"); - WriteStat(writer, value.LoadedBuiltinTargetInfo, value.LoadedCustomTargetInfo); - writer.WriteEndObject(); - writer.WriteStartObject("Executed"); - WriteStat(writer, value.ExecutedBuiltinTargetInfo, value.ExecutedCustomTargetInfo); - writer.WriteEndObject(); - writer.WriteEndObject(); - - void WriteStat(Utf8JsonWriter writer, TargetInfo builtinTargetsInfo, TargetInfo customTargetsInfo) - { - writer.WriteNumber(nameof(builtinTargetsInfo.Total), builtinTargetsInfo.Total + customTargetsInfo.Total); - WriteSingleStat(writer, builtinTargetsInfo, "Microsoft"); - WriteSingleStat(writer, customTargetsInfo, "Custom"); - } - - void WriteSingleStat(Utf8JsonWriter writer, TargetInfo targetInfo, string name) - { - if (targetInfo.Total > 0) - { - writer.WriteStartObject(name); - writer.WriteNumber(nameof(targetInfo.Total), targetInfo.Total); - writer.WriteNumber(nameof(targetInfo.FromNuget), targetInfo.FromNuget); - writer.WriteNumber(nameof(targetInfo.FromMetaproj), targetInfo.FromMetaproj); - writer.WriteEndObject(); - } - } - } - } - - private class TasksSummaryConverter : JsonConverter - { - /// - /// Processes task execution data to compile summary statistics for both built-in and custom tasks. - /// - /// Dictionary containing task execution data keyed by task identifiers. - public void Process(Dictionary tasksExecutionData) - { - foreach (KeyValuePair taskInfo in tasksExecutionData) - { - UpdateTaskStatistics(BuiltinTasksInfo, CustomTasksInfo, taskInfo.Key, taskInfo.Value); - } - } - - private void UpdateTaskStatistics( - TasksInfo builtinTaskInfo, - TasksInfo customTaskInfo, - TaskOrTargetTelemetryKey key, - TaskExecutionStats taskExecutionStats) - { - TasksInfo taskInfo = key.IsCustom ? customTaskInfo : builtinTaskInfo; - taskInfo.Total.Accumulate(taskExecutionStats); - - if (key.IsNuget) - { - taskInfo.FromNuget.Accumulate(taskExecutionStats); - } - } - - private TasksInfo BuiltinTasksInfo { get; } = new TasksInfo(); - - private TasksInfo CustomTasksInfo { get; } = new TasksInfo(); - - private class TasksInfo - { - public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty(); - - public TaskExecutionStats FromNuget { get; } = TaskExecutionStats.CreateEmpty(); - } - - public override TasksSummaryConverter? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - TasksSummaryConverter value, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - WriteStat(writer, value.BuiltinTasksInfo, "Microsoft"); - WriteStat(writer, value.CustomTasksInfo, "Custom"); - writer.WriteEndObject(); - - void WriteStat(Utf8JsonWriter writer, TasksInfo tasksInfo, string name) - { - writer.WriteStartObject(name); - WriteSingleStat(writer, tasksInfo.Total, nameof(tasksInfo.Total)); - WriteSingleStat(writer, tasksInfo.FromNuget, nameof(tasksInfo.FromNuget)); - writer.WriteEndObject(); - } - - void WriteSingleStat(Utf8JsonWriter writer, TaskExecutionStats stats, string name) - { - if (stats.ExecutionsCount > 0) - { - writer.WriteStartObject(name); - writer.WriteNumber(nameof(stats.ExecutionsCount), stats.ExecutionsCount); - writer.WriteNumber(nameof(stats.CumulativeExecutionTime.TotalMilliseconds), stats.CumulativeExecutionTime.TotalMilliseconds); - writer.WriteNumber(nameof(stats.TotalMemoryBytes), stats.TotalMemoryBytes); - writer.WriteEndObject(); - } - } - } - } - - private class NodeTelemetry : IActivityTelemetryDataHolder - { - private readonly IList _items; - - public NodeTelemetry(IList items) => _items = items; - - public IList GetActivityProperties() - => _items; - } - } -} diff --git a/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs b/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs index 717846204eb..92175ef4a71 100644 --- a/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs +++ b/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs @@ -4,6 +4,7 @@ using System; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Shared; @@ -55,16 +56,16 @@ public class TelemetryForwarder : ITelemetryForwarder // in future, this might be per event type public bool IsTelemetryCollected => true; - public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache) + public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache, string? taskFactoryName, string? taskHostRuntime) { var key = GetKey(name, isCustom, false, isFromNugetCache); - _workerNodeTelemetryData.AddTask(key, cumulativeExecutionTime, executionsCount, totalMemoryConsumed); + _workerNodeTelemetryData.AddTask(key, cumulativeExecutionTime, executionsCount, totalMemoryConsumed, taskFactoryName, taskHostRuntime); } - public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache) + public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache, TargetSkipReason skipReason = TargetSkipReason.None) { var key = GetKey(name, isCustom, isMetaproj, isFromNugetCache); - _workerNodeTelemetryData.AddTarget(key, wasExecuted); + _workerNodeTelemetryData.AddTarget(key, wasExecuted, skipReason); } private static TaskOrTargetTelemetryKey GetKey(string name, bool isCustom, bool isMetaproj, @@ -83,8 +84,9 @@ public class NullTelemetryForwarder : ITelemetryForwarder { public bool IsTelemetryCollected => false; - public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache) { } - public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache) { } + public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache, string? taskFactoryName, string? taskHostRuntime) { } + + public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache, TargetSkipReason skipReason = TargetSkipReason.None) { } public void FinalizeProcessing(LoggingContext loggingContext) { } } diff --git a/src/Build/Utilities/FileSpecMatchTester.cs b/src/Build/Utilities/FileSpecMatchTester.cs index 15380c2d5a0..76b19b3d1a4 100644 --- a/src/Build/Utilities/FileSpecMatchTester.cs +++ b/src/Build/Utilities/FileSpecMatchTester.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -131,7 +132,7 @@ private static void CreateRegexOrFilenamePattern(string unescapedFileSpec, strin ? Directory.GetCurrentDirectory() : FileUtilities.GetFullPathNoThrow(absoluteFixedDirPart); - normalizedFixedDirPart = FileUtilities.EnsureTrailingSlash(normalizedFixedDirPart); + normalizedFixedDirPart = FrameworkFileUtilities.EnsureTrailingSlash(normalizedFixedDirPart); var recombinedFileSpec = string.Concat(normalizedFixedDirPart, wildcardDirectoryPart, filenamePart); diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 73b91fe6c96..db68730447e 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -31,7 +31,6 @@ $(DefineConstants);FEATURE_ENVIRONMENT_SYSTEMDIRECTORY $(DefineConstants);FEATURE_FILE_TRACKER $(DefineConstants);FEATURE_GAC - $(DefineConstants);FEATURE_GET_COMMANDLINE $(DefineConstants);FEATURE_HANDLEPROCESSCORRUPTEDSTATEEXCEPTIONS $(DefineConstants);FEATURE_HTTP_LISTENER $(DefineConstants);FEATURE_INSTALLED_MSBUILD diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 6818b3b1965..a58a4bc167c 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -155,12 +155,39 @@ 4.5.4 4.5.1 4.5.0 + 4.5.0 + 4.7.1 + + + 9.0.11 + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + $(FrozenRuntimeVersionFor9) + + + + + + + + + + + @@ -175,20 +202,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs b/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs index 930ce27b496..88ffb519f0c 100644 --- a/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs +++ b/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs @@ -18,11 +18,16 @@ public void SerializationDeserializationTest() WorkerNodeTelemetryData td = new WorkerNodeTelemetryData( new Dictionary() { - { (TaskOrTargetTelemetryKey)"task1", new TaskExecutionStats(TimeSpan.FromMinutes(1), 5, 1234) }, - { (TaskOrTargetTelemetryKey)"task2", new TaskExecutionStats(TimeSpan.Zero, 0, 0) }, - { (TaskOrTargetTelemetryKey)"task3", new TaskExecutionStats(TimeSpan.FromTicks(1234), 12, 987654321) } + { (TaskOrTargetTelemetryKey)"task1", new TaskExecutionStats(TimeSpan.FromMinutes(1), 5, 1234, "AssemblyTaskFactory", "CLR4") }, + { (TaskOrTargetTelemetryKey)"task2", new TaskExecutionStats(TimeSpan.Zero, 0, 0, null, null) }, + { (TaskOrTargetTelemetryKey)"task3", new TaskExecutionStats(TimeSpan.FromTicks(1234), 12, 987654321, "CodeTaskFactory", "NET") } }, - new Dictionary() { { (TaskOrTargetTelemetryKey)"target1", false }, { (TaskOrTargetTelemetryKey)"target2", true }, }); + new Dictionary() + { + { (TaskOrTargetTelemetryKey)"target1", TargetExecutionStats.Skipped(TargetSkipReason.OutputsUpToDate) }, + { (TaskOrTargetTelemetryKey)"target2", TargetExecutionStats.Executed() }, + { (TaskOrTargetTelemetryKey)"target3", TargetExecutionStats.Skipped(TargetSkipReason.ConditionWasFalse) }, + }); WorkerNodeTelemetryEventArgs args = new WorkerNodeTelemetryEventArgs(td); diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs index 3760eec83e3..f25c79df899 100644 --- a/src/Framework/BinaryTranslator.cs +++ b/src/Framework/BinaryTranslator.cs @@ -118,11 +118,8 @@ public TranslationDirection Mode { return TranslationDirection.ReadFromStream; } } - /// - /// Gets or sets the packet version associated with the stream. - /// This can be used to exclude various fields from translation for backwards compatibility. - /// - public byte PacketVersion { get; set; } + /// + public byte NegotiatedPacketVersion { get; set; } /// /// Translates a boolean. @@ -1008,11 +1005,8 @@ public TranslationDirection Mode { return TranslationDirection.WriteToStream; } } - /// - /// Gets or sets the packet version associated with the stream. - /// This can be used to exclude various fields from translation for backwards compatibility. - /// - public byte PacketVersion { get; set; } + /// + public byte NegotiatedPacketVersion { get; set; } /// /// Translates a boolean. diff --git a/src/Framework/BuildErrorEventArgs.cs b/src/Framework/BuildErrorEventArgs.cs index 052d602f63d..97cb5b1f1df 100644 --- a/src/Framework/BuildErrorEventArgs.cs +++ b/src/Framework/BuildErrorEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/BuildMessageEventArgs.cs b/src/Framework/BuildMessageEventArgs.cs index cab1d21892b..20a1898bea2 100644 --- a/src/Framework/BuildMessageEventArgs.cs +++ b/src/Framework/BuildMessageEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using System.Runtime.Serialization; using Microsoft.Build.Shared; diff --git a/src/Framework/BuildWarningEventArgs.cs b/src/Framework/BuildWarningEventArgs.cs index 5e4d11acd39..543281e8c26 100644 --- a/src/Framework/BuildWarningEventArgs.cs +++ b/src/Framework/BuildWarningEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs index 66ff885a352..22049fdfc65 100644 --- a/src/Framework/ChangeWaves.cs +++ b/src/Framework/ChangeWaves.cs @@ -31,7 +31,8 @@ internal static class ChangeWaves internal static readonly Version Wave17_12 = new Version(17, 12); internal static readonly Version Wave17_14 = new Version(17, 14); internal static readonly Version Wave18_3 = new Version(18, 3); - internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3]; + internal static readonly Version Wave18_4 = new Version(18, 4); + internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4]; /// /// Special value indicating that all features behind all Change Waves should be enabled. diff --git a/src/Framework/CriticalBuildMessageEventArgs.cs b/src/Framework/CriticalBuildMessageEventArgs.cs index 07488b90833..613240347c4 100644 --- a/src/Framework/CriticalBuildMessageEventArgs.cs +++ b/src/Framework/CriticalBuildMessageEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif #nullable disable diff --git a/src/Framework/CustomBuildEventArgs.cs b/src/Framework/CustomBuildEventArgs.cs index 19f3f844776..85d59389d3f 100644 --- a/src/Framework/CustomBuildEventArgs.cs +++ b/src/Framework/CustomBuildEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif #nullable disable diff --git a/src/Framework/ErrorUtilities.cs b/src/Framework/ErrorUtilities.cs index c430b87dbf1..e9b9275d7c8 100644 --- a/src/Framework/ErrorUtilities.cs +++ b/src/Framework/ErrorUtilities.cs @@ -11,7 +11,7 @@ namespace Microsoft.Build.Framework // because some of the errors there will use localized resources from different assemblies, // which won't be referenceable in Framework. - internal class FrameworkErrorUtilities + internal static class FrameworkErrorUtilities { /// /// This method should be used in places where one would normally put diff --git a/src/Framework/ExtendedBuildErrorEventArgs.cs b/src/Framework/ExtendedBuildErrorEventArgs.cs index 2ce76e0bf53..54f558432b1 100644 --- a/src/Framework/ExtendedBuildErrorEventArgs.cs +++ b/src/Framework/ExtendedBuildErrorEventArgs.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/ExtendedBuildWarningEventArgs.cs b/src/Framework/ExtendedBuildWarningEventArgs.cs index 598526c40a8..2d9a163eb15 100644 --- a/src/Framework/ExtendedBuildWarningEventArgs.cs +++ b/src/Framework/ExtendedBuildWarningEventArgs.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs new file mode 100644 index 00000000000..23cc707631b --- /dev/null +++ b/src/Framework/FileUtilities.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework +{ + // TODO: this should be unified with Shared\FileUtilities, but it is hard to untangle everything in one go. + // Moved some of the methods here for now. + + /// + /// This class contains utility methods for file IO. + /// Functions from FileUtilities are transferred here as part of the effort to remove Shared files. + /// + internal static class FrameworkFileUtilities + { + internal static readonly char[] Slashes = ['/', '\\']; + + /// + /// Indicates if the given character is a slash. + /// + /// + /// true, if slash + internal static bool IsSlash(char c) + { + return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar); + } + + /// + /// Indicates if the given file-spec ends with a slash. + /// + /// The file spec. + /// true, if file-spec has trailing slash + internal static bool EndsWithSlash(string fileSpec) + { + return (fileSpec.Length > 0) + ? IsSlash(fileSpec[fileSpec.Length - 1]) + : false; + } + + internal static string FixFilePath(string path) + { + return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/'); + } + + /// + /// If the given path doesn't have a trailing slash then add one. + /// If the path is an empty string, does not modify it. + /// + /// The path to check. + /// A path with a slash. + internal static string EnsureTrailingSlash(string fileSpec) + { + fileSpec = FixFilePath(fileSpec); + if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1])) + { + fileSpec += Path.DirectorySeparatorChar; + } + + return fileSpec; + } + + /// + /// Ensures the path does not have a trailing slash. + /// + internal static string EnsureNoTrailingSlash(string path) + { + path = FixFilePath(path); + if (EndsWithSlash(path)) + { + path = path.Substring(0, path.Length - 1); + } + + return path; + } + +#if !TASKHOST + /// + /// If the given path doesn't have a trailing slash then add one. + /// + /// The absolute path to check. + /// An absolute path with a trailing slash. + internal static AbsolutePath EnsureTrailingSlash(AbsolutePath path) + { + return new AbsolutePath(EnsureTrailingSlash(path.Value), + original: path.OriginalValue, + ignoreRootedCheck: true); + } + + /// + /// Ensures the absolute path does not have a trailing slash. + /// + /// The absolute path to check. + /// An absolute path without a trailing slash. + internal static AbsolutePath EnsureNoTrailingSlash(AbsolutePath path) + { + return new AbsolutePath(EnsureNoTrailingSlash(path.Value), + original: path.OriginalValue, + ignoreRootedCheck: true); + } + + /// + /// Gets the canonicalized full path of the provided path. + /// Resolves relative segments like "." and "..". Fixes directory separators. + /// ASSUMES INPUT IS ALREADY UNESCAPED. + /// + internal static AbsolutePath NormalizePath(AbsolutePath path) + { + return new AbsolutePath(FixFilePath(Path.GetFullPath(path.Value)), + original: path.OriginalValue, + ignoreRootedCheck: true); + } + + /// + /// Resolves relative segments like "." and "..". + /// ASSUMES INPUT IS ALREADY UNESCAPED. + /// + internal static AbsolutePath RemoveRelativeSegments(AbsolutePath path) + { + return new AbsolutePath(Path.GetFullPath(path.Value), + original: path.OriginalValue, + ignoreRootedCheck: true); + } + + internal static AbsolutePath FixFilePath(AbsolutePath path) + { + return new AbsolutePath(FixFilePath(path.Value), + original: path.OriginalValue, + ignoreRootedCheck: true); + } +#endif + } +} \ No newline at end of file diff --git a/src/Framework/ITranslator.cs b/src/Framework/ITranslator.cs index 1e063ca1bd9..71059a49272 100644 --- a/src/Framework/ITranslator.cs +++ b/src/Framework/ITranslator.cs @@ -75,11 +75,17 @@ internal enum TranslationDirection internal interface ITranslator : IDisposable { /// - /// Gets or sets the packet version associated with the stream. - /// This can be used to exclude various fields from translation for backwards compatibility, - /// e.g. when Writer introduces information that should be skipped in the Reader stream. + /// Gets or sets the negotiated packet version between the communicating nodes. + /// This represents the minimum packet version supported by both the sender and receiver, + /// ensuring backward compatibility during cross-version communication. /// - byte PacketVersion { get; set; } + /// + /// This version is determined during the initial handshake between nodes and may differ + /// from NodePacketTypeExtensions.PacketVersion when nodes are running different MSBuild versions. + /// The negotiated version is used to conditionally serialize/deserialize fields that may + /// not be supported by older packet versions. + /// + byte NegotiatedPacketVersion { get; set; } /// /// Returns the current serialization mode. diff --git a/src/Framework/LazyFormattedBuildEventArgs.cs b/src/Framework/LazyFormattedBuildEventArgs.cs index 28598b2f1b6..5cb00f9a8b2 100644 --- a/src/Framework/LazyFormattedBuildEventArgs.cs +++ b/src/Framework/LazyFormattedBuildEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.Globalization; using System.IO; diff --git a/src/Framework/MSBuildEventSource.cs b/src/Framework/MSBuildEventSource.cs index 78c7eaa55f2..e6b691a2105 100644 --- a/src/Framework/MSBuildEventSource.cs +++ b/src/Framework/MSBuildEventSource.cs @@ -12,7 +12,7 @@ namespace Microsoft.Build.Eventing /// Changes to existing event method signatures will not be reflected unless you update the property or assign a new event ID. /// [EventSource(Name = "Microsoft-Build")] - internal sealed partial class MSBuildEventSource : EventSource + internal sealed class MSBuildEventSource : EventSource { public static class Keywords { @@ -40,6 +40,8 @@ public static class Keywords /// public static MSBuildEventSource Log = new MSBuildEventSource(); + private MSBuildEventSource() { } + #region Events /// diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 2f972d7903e..b87b9db871e 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -16,6 +16,14 @@ + + + + SourcePackages\Contracts\%(Link) + + + SourcePackages\Contracts\%(Link) + @@ -24,21 +32,19 @@ - - - + + - + - diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs index 1c5cc585e5b..7fb31c57232 100644 --- a/src/Framework/PathHelpers/AbsolutePath.cs +++ b/src/Framework/PathHelpers/AbsolutePath.cs @@ -19,7 +19,7 @@ namespace Microsoft.Build.Framework /// file system conventions (case-sensitive on Linux, case-insensitive on Windows and macOS). /// Does not perform any normalization beyond validating the path is fully qualified. /// A default instance (created via default(AbsolutePath)) has a null Value - /// and should not be used. Two default instances are considered equal. + /// and represents an issue in path handling. Two default instances are considered equal. /// public readonly struct AbsolutePath : IEquatable { @@ -32,6 +32,11 @@ namespace Microsoft.Build.Framework /// The normalized string representation of this path. /// public string Value { get; } + + /// + /// The original string used to create this path. + /// + public string OriginalValue { get; } /// /// Initializes a new instance of the struct. @@ -41,6 +46,7 @@ public AbsolutePath(string path) { ValidatePath(path); Value = path; + OriginalValue = path; } /// @@ -50,12 +56,24 @@ public AbsolutePath(string path) /// If true, skips checking whether the path is rooted. /// For internal and testing use, when we want to force bypassing the rooted check. internal AbsolutePath(string path, bool ignoreRootedCheck) + : this(path, path, ignoreRootedCheck) { - if (!ignoreRootedCheck) + } + + /// + /// Initializes a new instance of the struct. + /// + /// The absolute path string. + /// The original string used to create this path. + /// If true, skips checking whether the path is rooted. + internal AbsolutePath(string path, string original, bool ignoreRootedCheck) + { + if (!ignoreRootedCheck) { ValidatePath(path); } Value = path; + OriginalValue = original; } /// @@ -85,7 +103,14 @@ private static void ValidatePath(string path) /// /// The path to combine with the base path. /// The base path to combine with. - public AbsolutePath(string path, AbsolutePath basePath) => Value = Path.Combine(basePath.Value, path); + public AbsolutePath(string path, AbsolutePath basePath) + { + // This function should not throw when path has illegal characters. + // For .NET Framework, Microsoft.IO.Path.Combine should be used instead of System.IO.Path.Combine to achieve it. + // For .NET Core, System.IO.Path.Combine already does not throw in this case. + Value = Path.Combine(basePath.Value, path); + OriginalValue = path; + } /// /// Implicitly converts an AbsolutePath to a string. diff --git a/src/Framework/Polyfills/CallerArgumentExpressionAttribute.cs b/src/Framework/Polyfills/CallerArgumentExpressionAttribute.cs index 66e0e808c2c..91623fbd9f9 100644 --- a/src/Framework/Polyfills/CallerArgumentExpressionAttribute.cs +++ b/src/Framework/Polyfills/CallerArgumentExpressionAttribute.cs @@ -1,9 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NETCOREAPP3_0_OR_GREATER + +using System.Runtime.CompilerServices; + +// This is a supporting forwarder for an internal polyfill API +[assembly: TypeForwardedTo(typeof(CallerArgumentExpressionAttribute))] + +#else + namespace System.Runtime.CompilerServices; -#if !NET [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] internal sealed class CallerArgumentExpressionAttribute : Attribute { @@ -14,4 +22,5 @@ public CallerArgumentExpressionAttribute(string parameterName) public string ParameterName { get; } } + #endif diff --git a/src/Framework/Polyfills/StringSyntaxAttribute.cs b/src/Framework/Polyfills/StringSyntaxAttribute.cs index 4bfd1bb40cd..2be24ae6499 100644 --- a/src/Framework/Polyfills/StringSyntaxAttribute.cs +++ b/src/Framework/Polyfills/StringSyntaxAttribute.cs @@ -1,9 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !NET +#if NET7_0_OR_GREATER -using System; +using System.Runtime.CompilerServices; + +// This is a supporting forwarder for an internal polyfill API +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute))] + +#else + +namespace System.Diagnostics.CodeAnalysis; [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] internal sealed class StringSyntaxAttribute : Attribute @@ -67,4 +74,5 @@ public StringSyntaxAttribute(string syntax, params object?[] arguments) /// The syntax identifier for strings containing XML. public const string Xml = nameof(Xml); } + #endif diff --git a/src/Framework/ProjectImportedEventArgs.cs b/src/Framework/ProjectImportedEventArgs.cs index 2df59de35f3..4884d1bcf23 100644 --- a/src/Framework/ProjectImportedEventArgs.cs +++ b/src/Framework/ProjectImportedEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/TargetSkippedEventArgs.cs b/src/Framework/TargetSkippedEventArgs.cs index 6e636b4f61b..8536d2bec8a 100644 --- a/src/Framework/TargetSkippedEventArgs.cs +++ b/src/Framework/TargetSkippedEventArgs.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using Microsoft.Build.Shared; diff --git a/src/Framework/TaskEnvironment.cs b/src/Framework/TaskEnvironment.cs index 03895864491..ec18be1d817 100644 --- a/src/Framework/TaskEnvironment.cs +++ b/src/Framework/TaskEnvironment.cs @@ -33,7 +33,7 @@ public AbsolutePath ProjectDirectory /// /// Converts a relative or absolute path string to an absolute path. - /// This function resolves paths relative to ProjectDirectory. + /// This function resolves paths relative to . /// /// The path to convert. /// An absolute path representation. @@ -77,4 +77,4 @@ public AbsolutePath ProjectDirectory /// internal void Dispose() => _driver.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Framework/Telemetry/ActivityExtensions.cs b/src/Framework/Telemetry/ActivityExtensions.cs deleted file mode 100644 index 9b4e05f7c02..00000000000 --- a/src/Framework/Telemetry/ActivityExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.Build.Framework.Telemetry -{ - /// - /// Extension methods for . usage in VS OpenTelemetry. - /// - internal static class ActivityExtensions - { - /// - /// Add tags to the activity from a . - /// - public static Activity WithTags(this Activity activity, IActivityTelemetryDataHolder? dataHolder) - { - if (dataHolder != null) - { - activity.WithTags(dataHolder.GetActivityProperties()); - } - return activity; - } - - /// - /// Add tags to the activity from a list of TelemetryItems. - /// - public static Activity WithTags(this Activity activity, IList tags) - { - foreach (var tag in tags) - { - activity.WithTag(tag); - } - return activity; - } - /// - /// Add a tag to the activity from a . - /// - public static Activity WithTag(this Activity activity, TelemetryItem item) - { - object value = item.NeedsHashing ? GetHashed(item.Value) : item.Value; - activity.SetTag($"{TelemetryConstants.PropertyPrefix}{item.Name}", value); - return activity; - } - - /// - /// Set the start time of the activity. - /// - public static Activity WithStartTime(this Activity activity, DateTime? startTime) - { - if (startTime.HasValue) - { - activity.SetStartTime(startTime.Value); - } - return activity; - } - - /// - /// Depending on the platform, hash the value using an available mechanism. - /// - internal static string GetHashed(object value) - { - return Sha256Hasher.Hash(value.ToString() ?? ""); - } - - // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 - private static class Sha256Hasher - { - /// - /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) - /// - public static string Hash(string text) - { - byte[] bytes = Encoding.UTF8.GetBytes(text); -#if NET - byte[] hash = SHA256.HashData(bytes); -#if NET9_0_OR_GREATER - return Convert.ToHexStringLower(hash); -#else - return Convert.ToHexString(hash).ToLowerInvariant(); -#endif - -#else - // Create the SHA256 object and compute the hash - using (var sha256 = SHA256.Create()) - { - byte[] hash = sha256.ComputeHash(bytes); - - // Convert the hash bytes to a lowercase hex string (manual loop approach) - var sb = new StringBuilder(hash.Length * 2); - foreach (byte b in hash) - { - sb.AppendFormat("{0:x2}", b); - } - - return sb.ToString(); - } -#endif - } - - public static string HashWithNormalizedCasing(string text) - { - return Hash(text.ToUpperInvariant()); - } - } - } -} diff --git a/src/Framework/Telemetry/BuildCheckTelemetry.cs b/src/Framework/Telemetry/BuildCheckTelemetry.cs index 3b8507203c1..8555a1b33e8 100644 --- a/src/Framework/Telemetry/BuildCheckTelemetry.cs +++ b/src/Framework/Telemetry/BuildCheckTelemetry.cs @@ -87,10 +87,7 @@ internal class BuildCheckTelemetry yield return (RuleStatsEventName, properties); } - // set for the new submission in case of build server _submissionId = Guid.NewGuid(); } } - - diff --git a/src/Framework/Telemetry/BuildInsights.cs b/src/Framework/Telemetry/BuildInsights.cs new file mode 100644 index 00000000000..9ea8a5b5a88 --- /dev/null +++ b/src/Framework/Telemetry/BuildInsights.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; + +namespace Microsoft.Build.Framework.Telemetry; + +/// +/// Container for all build telemetry insights including tasks and targets details and summaries. +/// +internal sealed class BuildInsights +{ + public List Tasks { get; } + + public List Targets { get; } + + public TargetsSummaryInfo TargetsSummary { get; } + + public TasksSummaryInfo TasksSummary { get; } + + /// + /// Information about build incrementality classification. + /// + public BuildIncrementalityInfo? Incrementality { get; } + + public BuildInsights( + List tasks, + List targets, + TargetsSummaryInfo targetsSummary, + TasksSummaryInfo tasksSummary, + BuildIncrementalityInfo? incrementality = null) + { + Tasks = tasks; + Targets = targets; + TargetsSummary = targetsSummary; + TasksSummary = tasksSummary; + Incrementality = incrementality; + } + + internal record TasksSummaryInfo(TaskCategoryStats? Microsoft, TaskCategoryStats? Custom); + + internal record TaskCategoryStats(TaskStatsInfo? Total, TaskStatsInfo? FromNuget); + + internal record TaskStatsInfo(int ExecutionsCount, double TotalMilliseconds, long TotalMemoryBytes); + + internal record ErrorCountsInfo( + int? Compiler, + int? MsBuildGeneral, + int? MsBuildEvaluation, + int? MsBuildExecution, + int? MsBuildGraph, + int? Task, + int? SdkResolvers, + int? NetSdk, + int? NuGet, + int? BuildCheck, + int? NativeToolchain, + int? CodeAnalysis, + int? Razor, + int? Wpf, + int? AspNet, + int? Other); + + /// + /// Represents the type of build based on incrementality analysis. + /// + internal enum BuildType + { + /// + /// Build type could not be determined. + /// + Unknown, + + /// + /// Full build where most targets were executed. + /// + Full, + + /// + /// Incremental build where most targets were skipped due to up-to-date checks. + /// + Incremental + } + + /// + /// Information about build incrementality classification. + /// + /// The determined build type (Full, Incremental, or Unknown). + /// Total number of targets in the build. + /// Number of targets that were actually executed. + /// Number of targets that were skipped. + /// Number of targets skipped because outputs were up-to-date. + /// Number of targets skipped due to false conditions. + /// Number of targets skipped because they were previously built. + /// Ratio of skipped targets to total targets (0.0 to 1.0). Higher values indicate more incremental builds. + internal record BuildIncrementalityInfo( + BuildType Classification, + int TotalTargetsCount, + int ExecutedTargetsCount, + int SkippedTargetsCount, + int SkippedDueToUpToDateCount, + int SkippedDueToConditionCount, + int SkippedDueToPreviouslyBuiltCount, + double IncrementalityRatio); +} diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index c20c5817558..f035265c18a 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -1,9 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; +using System.Runtime.CompilerServices; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; namespace Microsoft.Build.Framework.Telemetry { @@ -32,6 +34,11 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public DateTime? InnerStartAt { get; set; } + /// + /// True if MSBuild runs from command line. + /// + public bool? IsStandaloneExecution { get; set; } + /// /// Time at which build have finished. /// @@ -100,138 +107,98 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public string? BuildEngineFrameworkName { get; set; } - public override IDictionary GetProperties() - { - var properties = new Dictionary(); + /// + /// Primary failure category when BuildSuccess = false. + /// One of: "Compiler", "MSBuildEngine", "Tasks", "SDKResolvers", "NETSDK", "NuGet", "BuildCheck", "Other". + /// + public string? FailureCategory { get; set; } - // populate property values - if (BuildEngineDisplayVersion != null) - { - properties[nameof(BuildEngineDisplayVersion)] = BuildEngineDisplayVersion; - } + /// + /// Error counts by category. + /// + public ErrorCountsInfo? ErrorCounts { get; set; } + + /// + /// Create a list of properties sent to VS telemetry. + /// + public Dictionary GetActivityProperties() + { + Dictionary telemetryItems = new(8); if (StartAt.HasValue && FinishedAt.HasValue) { - properties[TelemetryConstants.BuildDurationPropertyName] = (FinishedAt.Value - StartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + telemetryItems.Add(TelemetryConstants.BuildDurationPropertyName, (FinishedAt.Value - StartAt.Value).TotalMilliseconds); } if (InnerStartAt.HasValue && FinishedAt.HasValue) { - properties[TelemetryConstants.InnerBuildDurationPropertyName] = (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); - } - - if (BuildEngineFrameworkName != null) - { - properties[nameof(BuildEngineFrameworkName)] = BuildEngineFrameworkName; - } - - if (BuildEngineHost != null) - { - properties[nameof(BuildEngineHost)] = BuildEngineHost; - } - - if (InitialMSBuildServerState != null) - { - properties[nameof(InitialMSBuildServerState)] = InitialMSBuildServerState; - } - - if (ProjectPath != null) - { - properties[nameof(ProjectPath)] = ProjectPath; + telemetryItems.Add(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds); } - if (ServerFallbackReason != null) - { - properties[nameof(ServerFallbackReason)] = ServerFallbackReason; - } - - if (BuildSuccess.HasValue) - { - properties[nameof(BuildSuccess)] = BuildSuccess.Value.ToString(CultureInfo.InvariantCulture); - } - - if (BuildTarget != null) - { - properties[nameof(BuildTarget)] = BuildTarget; - } + AddIfNotNull(BuildEngineHost); + AddIfNotNull(BuildSuccess); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion); + AddIfNotNull(BuildCheckEnabled); + AddIfNotNull(MultiThreadedModeEnabled); + AddIfNotNull(SACEnabled); + AddIfNotNull(IsStandaloneExecution); + AddIfNotNull(FailureCategory); + AddIfNotNull(ErrorCounts); - if (BuildEngineVersion != null) - { - properties[nameof(BuildEngineVersion)] = BuildEngineVersion.ToString(); - } - - if (BuildCheckEnabled != null) - { - properties[nameof(BuildCheckEnabled)] = BuildCheckEnabled.Value.ToString(CultureInfo.InvariantCulture); - } - - if (MultiThreadedModeEnabled != null) - { - properties[nameof(MultiThreadedModeEnabled)] = MultiThreadedModeEnabled.Value.ToString(CultureInfo.InvariantCulture); - } + return telemetryItems; - if (SACEnabled != null) + void AddIfNotNull(object? value, [CallerArgumentExpression(nameof(value))] string key = "") { - properties[nameof(SACEnabled)] = SACEnabled.Value.ToString(CultureInfo.InvariantCulture); + if (value != null) + { + telemetryItems.Add(key, value); + } } - - return properties; } - /// - /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. - /// - /// - public IList GetActivityProperties() + public override IDictionary GetProperties() { - List telemetryItems = new(8); + var properties = new Dictionary(); + AddIfNotNull(BuildEngineDisplayVersion); + AddIfNotNull(BuildEngineFrameworkName); + AddIfNotNull(BuildEngineHost); + AddIfNotNull(InitialMSBuildServerState); + AddIfNotNull(ProjectPath); + AddIfNotNull(ServerFallbackReason); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion?.ToString(), nameof(BuildEngineVersion)); + AddIfNotNull(BuildSuccess?.ToString(), nameof(BuildSuccess)); + AddIfNotNull(BuildCheckEnabled?.ToString(), nameof(BuildCheckEnabled)); + AddIfNotNull(MultiThreadedModeEnabled?.ToString(), nameof(MultiThreadedModeEnabled)); + AddIfNotNull(SACEnabled?.ToString(), nameof(SACEnabled)); + AddIfNotNull(IsStandaloneExecution?.ToString(), nameof(IsStandaloneExecution)); + AddIfNotNull(FailureCategory); + AddIfNotNull(ErrorCounts?.ToString(), nameof(ErrorCounts)); + + // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) { - telemetryItems.Add(new TelemetryItem(TelemetryConstants.BuildDurationPropertyName, (FinishedAt.Value - StartAt.Value).TotalMilliseconds, false)); + properties[TelemetryConstants.BuildDurationPropertyName] = + (FinishedAt.Value - StartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } if (InnerStartAt.HasValue && FinishedAt.HasValue) { - telemetryItems.Add(new TelemetryItem(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds, false)); - } - - if (BuildEngineHost != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildEngineHost), BuildEngineHost, false)); + properties[TelemetryConstants.InnerBuildDurationPropertyName] = + (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } - if (BuildSuccess.HasValue) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildSuccess), BuildSuccess, false)); - } - - if (BuildTarget != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildTarget), BuildTarget, true)); - } - - if (BuildEngineVersion != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildEngineVersion), BuildEngineVersion.ToString(), false)); - } - - if (BuildCheckEnabled != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildCheckEnabled), BuildCheckEnabled, false)); - } - - if (MultiThreadedModeEnabled != null) - { - telemetryItems.Add(new TelemetryItem(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled, false)); - } + return properties; - if (SACEnabled != null) + void AddIfNotNull(string? value, [CallerArgumentExpression(nameof(value))] string key = "") { - telemetryItems.Add(new TelemetryItem(nameof(SACEnabled), SACEnabled, false)); + if (value != null) + { + properties[key] = value; + } } - - return telemetryItems; } } } diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs new file mode 100644 index 00000000000..3bb2ed30f8e --- /dev/null +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETFRAMEWORK + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Wraps a and implements . + /// + internal class DiagnosticActivity : IActivity + { + private readonly Activity _activity; + private bool _disposed; + + public DiagnosticActivity(Activity activity) + { + _activity = activity; + } + + public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + { + Dictionary? tags = dataHolder?.GetActivityProperties(); + if (tags != null) + { + foreach (KeyValuePair tag in tags) + { + SetTag(tag.Key, tag.Value); + } + } + + return this; + } + + public IActivity? SetTag(string key, object? value) + { + if (value != null) + { + _activity.SetTag($"{TelemetryConstants.PropertyPrefix}{key}", value); + } + + return this; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _activity.Dispose(); + + _disposed = true; + } + } +} + +#endif diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs new file mode 100644 index 00000000000..6118e50f7e8 --- /dev/null +++ b/src/Framework/Telemetry/IActivity.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Represents an activity for telemetry tracking. + /// + internal interface IActivity : IDisposable + { + /// + /// Sets a tag on the activity. + /// + /// Telemetry data holder. + /// The activity instance for method chaining. + IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); + + /// + /// Sets a tag on the activity. + /// + /// The tag key. + /// The tag value. + /// The activity instance for method chaining. + IActivity? SetTag(string key, object? value); + } +} diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 9eeb0a7509f..e660f191695 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; namespace Microsoft.Build.Framework.Telemetry; /// -/// Interface for classes that hold telemetry data that should be added as tags to an . +/// Interface for classes that hold telemetry data that should be added as tags to an . /// internal interface IActivityTelemetryDataHolder { - IList GetActivityProperties(); + Dictionary GetActivityProperties(); } diff --git a/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs b/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs index a0303e4a4e2..1aa5dea0025 100644 --- a/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs +++ b/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs @@ -8,5 +8,34 @@ namespace Microsoft.Build.Framework.Telemetry; internal interface IWorkerNodeTelemetryData { Dictionary TasksExecutionData { get; } - Dictionary TargetsExecutionData { get; } + + Dictionary TargetsExecutionData { get; } +} + +/// +/// Represents the execution statistics of a target. +/// +/// Whether the target was executed (not skipped). +/// The reason the target was skipped, if applicable. +internal readonly struct TargetExecutionStats(bool wasExecuted, TargetSkipReason skipReason = TargetSkipReason.None) +{ + /// + /// Whether the target was executed (not skipped). + /// + public bool WasExecuted { get; } = wasExecuted; + + /// + /// The reason the target was skipped. Only meaningful when is false. + /// + public TargetSkipReason SkipReason { get; } = skipReason; + + /// + /// Creates stats for an executed target. + /// + public static TargetExecutionStats Executed() => new(wasExecuted: true); + + /// + /// Creates stats for a skipped target with the given reason. + /// + public static TargetExecutionStats Skipped(TargetSkipReason reason) => new(wasExecuted: false, skipReason: reason); } diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index 7d73f87062f..891e85c781f 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -1,36 +1,62 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; +#else using System.Diagnostics; +#endif namespace Microsoft.Build.Framework.Telemetry { /// - /// Wrapper class for ActivitySource with a method that wraps Activity name with VS OTel prefix. + /// Wrapper class for ActivitySource with a method that wraps Activity name with MSBuild prefix. + /// On .NET Framework, activities are also forwarded to VS Telemetry. /// internal class MSBuildActivitySource { +#if NETFRAMEWORK + private readonly TelemetrySession? _telemetrySession; + + public MSBuildActivitySource(TelemetrySession? telemetrySession) + { + _telemetrySession = telemetrySession; + } +#else private readonly ActivitySource _source; - private readonly double _sampleRate; - public MSBuildActivitySource(string name, double sampleRate) + public MSBuildActivitySource(string name) { _source = new ActivitySource(name); - _sampleRate = sampleRate; } +#endif + /// - /// Prefixes activity with VS OpenTelemetry. + /// Starts a new activity with the appropriate telemetry prefix. /// /// Name of the telemetry event without prefix. - /// - public Activity? StartActivity(string name) + /// An wrapping the underlying Activity, or null if not sampled. + public IActivity? StartActivity(string name) { - var activity = Activity.Current?.HasRemoteParent == true - ? _source.StartActivity($"{TelemetryConstants.EventPrefix}{name}", ActivityKind.Internal, parentId: Activity.Current.ParentId) - : _source.StartActivity($"{TelemetryConstants.EventPrefix}{name}"); - activity?.WithTag(new("SampleRate", _sampleRate, false)); + string eventName = $"{TelemetryConstants.EventPrefix}{name}"; + +#if NETFRAMEWORK + TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); + return operation != null ? new VsTelemetryActivity(operation) : null; +#else + Activity? activity = Activity.Current?.HasRemoteParent == true + ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) + : _source.StartActivity(eventName); + + if (activity == null) + { + return null; + } + + activity.SetTag("SampleRate", TelemetryConstants.DefaultSampleRate); - return activity; + return new DiagnosticActivity(activity); +#endif } } } diff --git a/src/Framework/Telemetry/OpenTelemetryManager.cs b/src/Framework/Telemetry/OpenTelemetryManager.cs deleted file mode 100644 index 7ee7813bddf..00000000000 --- a/src/Framework/Telemetry/OpenTelemetryManager.cs +++ /dev/null @@ -1,282 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK -using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions; -using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions.Exporters; -using Microsoft.VisualStudio.OpenTelemetry.Collector.Interfaces; -using Microsoft.VisualStudio.OpenTelemetry.Collector.Settings; -using OpenTelemetry; -using OpenTelemetry.Trace; -#endif -using System; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace Microsoft.Build.Framework.Telemetry -{ - - /// - /// Singleton class for configuring and managing the telemetry infrastructure with System.Diagnostics.Activity, - /// OpenTelemetry SDK, and VS OpenTelemetry Collector. - /// - internal class OpenTelemetryManager - { - // Lazy provides thread-safe lazy initialization. - private static readonly Lazy s_instance = - new Lazy(() => new OpenTelemetryManager(), LazyThreadSafetyMode.ExecutionAndPublication); - - /// - /// Globally accessible instance of . - /// - public static OpenTelemetryManager Instance => s_instance.Value; - - private TelemetryState _telemetryState = TelemetryState.Uninitialized; - private readonly LockType _initializeLock = new LockType(); - private double _sampleRate = TelemetryConstants.DefaultSampleRate; - -#if NETFRAMEWORK - private TracerProvider? _tracerProvider; - private IOpenTelemetryCollector? _collector; -#endif - - public string? LoadFailureExceptionMessage { get; set; } - - /// - /// Optional activity source for MSBuild or other telemetry usage. - /// - public MSBuildActivitySource? DefaultActivitySource { get; private set; } - - private OpenTelemetryManager() - { - } - - /// - /// Initializes the telemetry infrastructure. Multiple invocations are no-op, thread-safe. - /// - /// Differentiates between executing as MSBuild.exe or from VS/API. - public void Initialize(bool isStandalone) - { - // for lock free early exit - if (_telemetryState != TelemetryState.Uninitialized) - { - return; - } - - lock (_initializeLock) - { - // for correctness - if (_telemetryState != TelemetryState.Uninitialized) - { - return; - } - - if (IsOptOut()) - { - _telemetryState = TelemetryState.OptOut; - return; - } - - // TODO: temporary until we have green light to enable telemetry perf-wise - if (!IsOptIn()) - { - _telemetryState = TelemetryState.Unsampled; - return; - } - - if (!IsSampled()) - { - _telemetryState = TelemetryState.Unsampled; - return; - } - - InitializeActivitySources(); - } -#if NETFRAMEWORK - try - { - InitializeTracerProvider(); - - // TODO: Enable commented logic when Collector is present in VS - // if (isStandalone) - InitializeCollector(); - - // } - } - catch (Exception ex) when (ex is System.IO.FileNotFoundException or System.IO.FileLoadException) - { - // catch exceptions from loading the OTel SDK or Collector to maintain usability of Microsoft.Build.Framework package in our and downstream tests in VS. - _telemetryState = TelemetryState.Unsampled; - LoadFailureExceptionMessage = ex.ToString(); - return; - } -#endif - } - - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeActivitySources() - { - _telemetryState = TelemetryState.TracerInitialized; - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _sampleRate); - } - -#if NETFRAMEWORK - /// - /// Initializes the OpenTelemetry SDK TracerProvider with VS default exporter settings. - /// - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeTracerProvider() - { - var exporterSettings = OpenTelemetryExporterSettingsBuilder - .CreateVSDefault(TelemetryConstants.VSMajorVersion) - .Build(); - - TracerProviderBuilder tracerProviderBuilder = Sdk - .CreateTracerProviderBuilder() - // this adds listeners to ActivitySources with the prefix "Microsoft.VisualStudio.OpenTelemetry." - .AddVisualStudioDefaultTraceExporter(exporterSettings); - - _tracerProvider = tracerProviderBuilder.Build(); - _telemetryState = TelemetryState.ExporterInitialized; - } - - /// - /// Initializes the VS OpenTelemetry Collector with VS default settings. - /// - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeCollector() - { - IOpenTelemetryCollectorSettings collectorSettings = OpenTelemetryCollectorSettingsBuilder - .CreateVSDefault(TelemetryConstants.VSMajorVersion) - .Build(); - - _collector = OpenTelemetryCollectorProvider.CreateCollector(collectorSettings); - _collector.StartAsync().GetAwaiter().GetResult(); - - _telemetryState = TelemetryState.CollectorInitialized; - } -#endif - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void ForceFlushInner() - { -#if NETFRAMEWORK - _tracerProvider?.ForceFlush(); -#endif - } - - /// - /// Flush the telemetry in TracerProvider/Exporter. - /// - public void ForceFlush() - { - if (ShouldBeCleanedUp()) - { - ForceFlushInner(); - } - } - - // to avoid assembly loading OpenTelemetry in tests - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void ShutdownInner() - { -#if NETFRAMEWORK - _tracerProvider?.Shutdown(); - // Dispose stops the collector, with a default drain timeout of 10s - _collector?.Dispose(); -#endif - } - - /// - /// Shuts down the telemetry infrastructure. - /// - public void Shutdown() - { - lock (_initializeLock) - { - if (ShouldBeCleanedUp()) - { - ShutdownInner(); - } - - _telemetryState = TelemetryState.Disposed; - } - } - - /// - /// Determines if the user has explicitly opted out of telemetry. - /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14); - - /// - /// TODO: Temporary until perf of loading OTel is agreed to in VS. - /// - private bool IsOptIn() => !IsOptOut() && (Traits.Instance.TelemetryOptIn || Traits.Instance.TelemetrySampleRateOverride.HasValue); - - /// - /// Determines if telemetry should be initialized based on sampling and environment variable overrides. - /// - private bool IsSampled() - { - double? overrideRate = Traits.Instance.TelemetrySampleRateOverride; - if (overrideRate.HasValue) - { - _sampleRate = overrideRate.Value; - } - else - { -#if !NETFRAMEWORK - // In core, OTel infrastructure is not initialized by default. - return false; -#endif - } - - // Simple random sampling, this method is called once, no need to save the Random instance. - Random random = new(); - return random.NextDouble() < _sampleRate; - } - - private bool ShouldBeCleanedUp() => _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized; - - internal bool IsActive() => _telemetryState == TelemetryState.TracerInitialized || _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized; - - /// - /// State of the telemetry infrastructure. - /// - internal enum TelemetryState - { - /// - /// Initial state. - /// - Uninitialized, - - /// - /// Opt out of telemetry. - /// - OptOut, - - /// - /// Run not sampled for telemetry. - /// - Unsampled, - - /// - /// For core hook, ActivitySource is created. - /// - TracerInitialized, - - /// - /// For VS scenario with a collector. ActivitySource, OTel TracerProvider are created. - /// - ExporterInitialized, - - /// - /// For standalone, ActivitySource, OTel TracerProvider, VS OpenTelemetry Collector are created. - /// - CollectorInitialized, - - /// - /// End state. - /// - Disposed - } - } -} diff --git a/src/Framework/Telemetry/TaskExecutionStats.cs b/src/Framework/Telemetry/TaskExecutionStats.cs index 533599734fd..a9dac1ab4bb 100644 --- a/src/Framework/Telemetry/TaskExecutionStats.cs +++ b/src/Framework/Telemetry/TaskExecutionStats.cs @@ -8,10 +8,15 @@ namespace Microsoft.Build.Framework.Telemetry; /// /// Represents the execution statistics of tasks executed on a node. /// -internal class TaskExecutionStats(TimeSpan cumulativeExecutionTime, int executionsCount, long totalMemoryConsumption) +internal class TaskExecutionStats( + TimeSpan cumulativeExecutionTime, + int executionsCount, + long totalMemoryConsumption, + string? taskFactoryName, + string? taskHostRuntime) { private TaskExecutionStats() - : this(TimeSpan.Zero, 0, 0) + : this(TimeSpan.Zero, 0, 0, null, null) { } /// @@ -36,15 +41,30 @@ internal static TaskExecutionStats CreateEmpty() /// public int ExecutionsCount { get; set; } = executionsCount; + /// + /// The name of the task factory used to create this task. + /// Examples: AssemblyTaskFactory, IntrinsicTaskFactory, CodeTaskFactory, + /// RoslynCodeTaskFactory, XamlTaskFactory, or a custom factory name. + /// + public string? TaskFactoryName { get; set; } = taskFactoryName; + + /// + /// The runtime specified for out-of-process task execution. + /// Values: "CLR2", "CLR4", "NET", or null if not specified. + /// + public string? TaskHostRuntime { get; set; } = taskHostRuntime; + /// /// Accumulates statistics from another instance into this one. /// /// Statistics to add to this instance. internal void Accumulate(TaskExecutionStats other) { - this.CumulativeExecutionTime += other.CumulativeExecutionTime; - this.TotalMemoryBytes += other.TotalMemoryBytes; - this.ExecutionsCount += other.ExecutionsCount; + CumulativeExecutionTime += other.CumulativeExecutionTime; + TotalMemoryBytes += other.TotalMemoryBytes; + ExecutionsCount += other.ExecutionsCount; + TaskFactoryName ??= other.TaskFactoryName; + TaskHostRuntime ??= other.TaskHostRuntime; } // We need custom Equals for easier assertions in tests @@ -60,7 +80,9 @@ public override bool Equals(object? obj) protected bool Equals(TaskExecutionStats other) => CumulativeExecutionTime.Equals(other.CumulativeExecutionTime) && TotalMemoryBytes == other.TotalMemoryBytes && - ExecutionsCount == other.ExecutionsCount; + ExecutionsCount == other.ExecutionsCount && + TaskFactoryName == other.TaskFactoryName && + TaskHostRuntime == other.TaskHostRuntime; // Needed since we override Equals public override int GetHashCode() @@ -70,6 +92,8 @@ public override int GetHashCode() var hashCode = CumulativeExecutionTime.GetHashCode(); hashCode = (hashCode * 397) ^ TotalMemoryBytes.GetHashCode(); hashCode = (hashCode * 397) ^ ExecutionsCount.GetHashCode(); + hashCode = (hashCode * 397) ^ (TaskFactoryName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (TaskHostRuntime?.GetHashCode() ?? 0); return hashCode; } } diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index dc51085f60c..94e194b9e48 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -3,20 +3,10 @@ namespace Microsoft.Build.Framework.Telemetry; /// -/// Constants for VS OpenTelemetry for basic configuration and appropriate naming for VS exporting/collection. +/// Constants for VS Telemetry for basic configuration and appropriate naming for VS exporting/collection. /// internal static class TelemetryConstants { - /// - /// "Microsoft.VisualStudio.OpenTelemetry.*" namespace is required by VS exporting/collection. - /// - public const string ActivitySourceNamespacePrefix = "Microsoft.VisualStudio.OpenTelemetry.MSBuild."; - - /// - /// Namespace of the default ActivitySource handling e.g. End of build telemetry. - /// - public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; - /// /// Prefix required by VS exporting/collection. /// @@ -28,9 +18,14 @@ internal static class TelemetryConstants public const string PropertyPrefix = "VS.MSBuild."; /// - /// For VS OpenTelemetry Collector to apply the correct privacy policy. + /// "Microsoft.Build.Telemetry.*" namespace is required by VS exporting/collection. /// - public const string VSMajorVersion = "18.0"; + public const string ActivitySourceNamespacePrefix = "Microsoft.Build.Telemetry"; + + /// + /// Namespace of the default ActivitySource handling e.g. End of build telemetry. + /// + public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; /// /// Sample rate for the default namespace. @@ -47,13 +42,9 @@ internal static class TelemetryConstants /// Name of the property for inner build duration. /// public const string InnerBuildDurationPropertyName = "InnerBuildDurationInMilliseconds"; -} -internal static class NodeTelemetryTags -{ - // These properties can't use nameof since they're not tied to a specific class property - public const string Tasks = "Tasks"; - public const string Targets = "Targets"; - public const string TargetsSummary = "TargetsSummary"; - public const string TasksSummary = "TasksSummary"; + /// + /// Name of the property for build activity. + /// + public const string Build = "Build"; } diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs new file mode 100644 index 00000000000..782561ddc52 --- /dev/null +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -0,0 +1,415 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; + +namespace Microsoft.Build.Framework.Telemetry +{ + internal static class TelemetryDataUtils + { + /// + /// Known Microsoft task factory type names that should not be hashed. + /// + private static readonly HashSet KnownTaskFactoryNames = new(StringComparer.OrdinalIgnoreCase) + { + "AssemblyTaskFactory", + "TaskHostFactory", + "CodeTaskFactory", + "RoslynCodeTaskFactory", + "XamlTaskFactory", + "IntrinsicTaskFactory", + }; + + /// + /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. + /// + /// Data about tasks and target forwarded from nodes. + /// Controls whether Task details should attached to the telemetry. + /// Controls whether Target details should be attached to the telemetry. + /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . + public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails) + { + if (telemetryData == null) + { + return null; + } + + var targetsSummary = new TargetsSummaryConverter(); + targetsSummary.Process(telemetryData.TargetsExecutionData); + + var tasksSummary = new TasksSummaryConverter(); + tasksSummary.Process(telemetryData.TasksExecutionData); + + var incrementality = ComputeIncrementalityInfo(telemetryData.TargetsExecutionData); + + var buildInsights = new BuildInsights( + includeTasksDetails ? GetTasksDetails(telemetryData.TasksExecutionData) : [], + includeTargetDetails ? GetTargetsDetails(telemetryData.TargetsExecutionData) : [], + GetTargetsSummary(targetsSummary), + GetTasksSummary(tasksSummary), + incrementality); + + return new NodeTelemetry(buildInsights); + } + + /// + /// Converts targets details to a list of custom objects for telemetry. + /// + private static List GetTargetsDetails(Dictionary targetsDetails) + { + var result = new List(); + + foreach (KeyValuePair valuePair in targetsDetails) + { + string targetName = ShouldHashKey(valuePair.Key) ? GetHashed(valuePair.Key.Name) : valuePair.Key.Name; + + result.Add(new TargetDetailInfo( + targetName, + valuePair.Value.WasExecuted, + valuePair.Key.IsCustom, + valuePair.Key.IsNuget, + valuePair.Key.IsMetaProj, + valuePair.Value.SkipReason)); + } + + return result; + + static bool ShouldHashKey(TaskOrTargetTelemetryKey key) => key.IsCustom || key.IsMetaProj; + } + + internal record TargetDetailInfo(string Name, bool WasExecuted, bool IsCustom, bool IsNuget, bool IsMetaProj, TargetSkipReason SkipReason); + + /// + /// Converts tasks details to a list of custom objects for telemetry. + /// + private static List GetTasksDetails( + Dictionary tasksDetails) + { + var result = new List(); + + foreach (KeyValuePair valuePair in tasksDetails) + { + string taskName = valuePair.Key.IsCustom ? GetHashed(valuePair.Key.Name) : valuePair.Key.Name; + string? factoryName = GetFactoryNameForTelemetry(valuePair.Value.TaskFactoryName); + + result.Add(new TaskDetailInfo( + taskName, + valuePair.Value.CumulativeExecutionTime.TotalMilliseconds, + valuePair.Value.ExecutionsCount, + valuePair.Value.TotalMemoryBytes, + valuePair.Key.IsCustom, + valuePair.Key.IsNuget, + factoryName, + valuePair.Value.TaskHostRuntime)); + } + + return result; + } + + /// + /// Gets the factory name for telemetry, hashing custom factory names. + /// + private static string? GetFactoryNameForTelemetry(string? factoryName) + { + if (string.IsNullOrEmpty(factoryName)) + { + return null; + } + + return KnownTaskFactoryNames.Contains(factoryName!) ? factoryName : GetHashed(factoryName!); + } + + /// + /// Depending on the platform, hash the value using an available mechanism. + /// + internal static string GetHashed(object value) => Sha256Hasher.Hash(value?.ToString() ?? ""); + + // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 + private static class Sha256Hasher + { + /// + /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) + /// + public static string Hash(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); +#if NET + byte[] hash = SHA256.HashData(bytes); +#if NET9_0_OR_GREATER + return System.Convert.ToHexStringLower(hash); +#else + return Convert.ToHexString(hash).ToLowerInvariant(); +#endif + +#else + // Create the SHA256 object and compute the hash + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(bytes); + + // Convert the hash bytes to a lowercase hex string (manual loop approach) + var sb = new StringBuilder(hash.Length * 2); + foreach (byte b in hash) + { + sb.AppendFormat("{0:x2}", b); + } + + return sb.ToString(); + } +#endif + } + } + + internal record TaskDetailInfo(string Name, double TotalMilliseconds, int ExecutionsCount, long TotalMemoryBytes, bool IsCustom, bool IsNuget, string? FactoryName, string? TaskHostRuntime); + + /// + /// Converts targets summary to a custom object for telemetry. + /// + private static TargetsSummaryInfo GetTargetsSummary(TargetsSummaryConverter summary) + { + return new TargetsSummaryInfo( + CreateTargetStats(summary.LoadedBuiltinTargetInfo, summary.LoadedCustomTargetInfo), + CreateTargetStats(summary.ExecutedBuiltinTargetInfo, summary.ExecutedCustomTargetInfo)); + + static TargetStatsInfo CreateTargetStats( + TargetsSummaryConverter.TargetInfo builtinInfo, + TargetsSummaryConverter.TargetInfo customInfo) + { + var microsoft = builtinInfo.Total > 0 + ? new TargetCategoryInfo(builtinInfo.Total, builtinInfo.FromNuget, builtinInfo.FromMetaproj) + : null; + + var custom = customInfo.Total > 0 + ? new TargetCategoryInfo(customInfo.Total, customInfo.FromNuget, customInfo.FromMetaproj) + : null; + + return new TargetStatsInfo(builtinInfo.Total + customInfo.Total, microsoft, custom); + } + } + + internal record TargetsSummaryInfo(TargetStatsInfo Loaded, TargetStatsInfo Executed); + + internal record TargetStatsInfo(int Total, TargetCategoryInfo? Microsoft, TargetCategoryInfo? Custom); + + internal record TargetCategoryInfo(int Total, int FromNuget, int FromMetaproj); + + /// + /// Converts tasks summary to a custom object for telemetry. + /// + private static TasksSummaryInfo GetTasksSummary(TasksSummaryConverter summary) + { + var microsoft = CreateTaskStats(summary.BuiltinTasksInfo.Total, summary.BuiltinTasksInfo.FromNuget); + var custom = CreateTaskStats(summary.CustomTasksInfo.Total, summary.CustomTasksInfo.FromNuget); + + return new TasksSummaryInfo(microsoft, custom); + + static TaskCategoryStats? CreateTaskStats(TaskExecutionStats total, TaskExecutionStats fromNuget) + { + var totalStats = total.ExecutionsCount > 0 + ? new TaskStatsInfo( + total.ExecutionsCount, + total.CumulativeExecutionTime.TotalMilliseconds, + total.TotalMemoryBytes) + : null; + + var nugetStats = fromNuget.ExecutionsCount > 0 + ? new TaskStatsInfo( + fromNuget.ExecutionsCount, + fromNuget.CumulativeExecutionTime.TotalMilliseconds, + fromNuget.TotalMemoryBytes) + : null; + + return (totalStats != null || nugetStats != null) + ? new TaskCategoryStats(totalStats, nugetStats) + : null; + } + } + + private class TargetsSummaryConverter + { + internal TargetInfo LoadedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo LoadedCustomTargetInfo { get; } = new(); + + internal TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo ExecutedCustomTargetInfo { get; } = new(); + + /// + /// Processes target execution data to compile summary statistics for both built-in and custom targets. + /// + public void Process(Dictionary targetsExecutionData) + { + foreach (var kv in targetsExecutionData) + { + GetTargetInfo(kv.Key, isExecuted: false).Increment(kv.Key); + + // Update executed targets statistics (only if executed) + if (kv.Value.WasExecuted) + { + GetTargetInfo(kv.Key, isExecuted: true).Increment(kv.Key); + } + } + } + + private TargetInfo GetTargetInfo(TaskOrTargetTelemetryKey key, bool isExecuted) => + (key.IsCustom, isExecuted) switch + { + (true, true) => ExecutedCustomTargetInfo, + (true, false) => LoadedCustomTargetInfo, + (false, true) => ExecutedBuiltinTargetInfo, + (false, false) => LoadedBuiltinTargetInfo, + }; + + internal class TargetInfo + { + public int Total { get; private set; } + + public int FromNuget { get; private set; } + + public int FromMetaproj { get; private set; } + + internal void Increment(TaskOrTargetTelemetryKey key) + { + Total++; + if (key.IsNuget) + { + FromNuget++; + } + + if (key.IsMetaProj) + { + FromMetaproj++; + } + } + } + } + + private class TasksSummaryConverter + { + internal TasksInfo BuiltinTasksInfo { get; } = new(); + + internal TasksInfo CustomTasksInfo { get; } = new(); + + /// + /// Processes task execution data to compile summary statistics for both built-in and custom tasks. + /// + public void Process(Dictionary tasksExecutionData) + { + foreach (KeyValuePair kv in tasksExecutionData) + { + var taskInfo = kv.Key.IsCustom ? CustomTasksInfo : BuiltinTasksInfo; + taskInfo.Total.Accumulate(kv.Value); + + if (kv.Key.IsNuget) + { + taskInfo.FromNuget.Accumulate(kv.Value); + } + } + } + + internal class TasksInfo + { + public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty(); + + public TaskExecutionStats FromNuget { get; } = TaskExecutionStats.CreateEmpty(); + } + } + + /// + /// Threshold ratio above which a build is classified as incremental. + /// A build with more than 70% skipped targets is considered incremental. + /// + private const double IncrementalBuildThreshold = 0.70; + + /// + /// Computes build incrementality information from target execution data. + /// + private static BuildInsights.BuildIncrementalityInfo ComputeIncrementalityInfo( + Dictionary targetsExecutionData) + { + int totalTargets = targetsExecutionData.Count; + int executedTargets = 0; + int skippedTargets = 0; + int skippedDueToUpToDate = 0; + int skippedDueToCondition = 0; + int skippedDueToPreviouslyBuilt = 0; + + foreach (var kv in targetsExecutionData) + { + if (kv.Value.WasExecuted) + { + executedTargets++; + } + else + { + skippedTargets++; + _ = kv.Value.SkipReason switch + { + TargetSkipReason.OutputsUpToDate => skippedDueToUpToDate++, + TargetSkipReason.ConditionWasFalse => skippedDueToCondition++, + TargetSkipReason.PreviouslyBuiltSuccessfully or TargetSkipReason.PreviouslyBuiltUnsuccessfully => skippedDueToPreviouslyBuilt++, + _ => 0 + }; + } + } + + // Calculate incrementality ratio (0.0 = full build, 1.0 = fully incremental) + double incrementalityRatio = totalTargets > 0 ? (double)skippedTargets / totalTargets : 0.0; + + var classification = totalTargets == 0 + ? BuildInsights.BuildType.Unknown + : incrementalityRatio >= IncrementalBuildThreshold + ? BuildInsights.BuildType.Incremental + : BuildInsights.BuildType.Full; + + return new BuildInsights.BuildIncrementalityInfo( + Classification: classification, + TotalTargetsCount: totalTargets, + ExecutedTargetsCount: executedTargets, + SkippedTargetsCount: skippedTargets, + SkippedDueToUpToDateCount: skippedDueToUpToDate, + SkippedDueToConditionCount: skippedDueToCondition, + SkippedDueToPreviouslyBuiltCount: skippedDueToPreviouslyBuilt, + IncrementalityRatio: incrementalityRatio); + } + + private sealed class NodeTelemetry(BuildInsights insights) : IActivityTelemetryDataHolder + { + Dictionary IActivityTelemetryDataHolder.GetActivityProperties() + { + Dictionary properties = new(5) + { + [nameof(BuildInsights.TargetsSummary)] = insights.TargetsSummary, + [nameof(BuildInsights.TasksSummary)] = insights.TasksSummary, + }; + + AddIfNotEmpty(nameof(BuildInsights.Targets), insights.Targets); + AddIfNotEmpty(nameof(BuildInsights.Tasks), insights.Tasks); + AddIfNotNull(nameof(BuildInsights.Incrementality), insights.Incrementality); + + return properties; + + void AddIfNotEmpty(string key, List list) + { + if (list.Count > 0) + { + properties[key] = list; + } + } + + void AddIfNotNull(string key, object? value) + { + if (value != null) + { + properties[key] = value; + } + } + } + } + } +} diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs deleted file mode 100644 index f037d7ddbea..00000000000 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Build.Framework.Telemetry; - -internal record TelemetryItem(string Name, object Value, bool NeedsHashing); diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs new file mode 100644 index 00000000000..4b55507436b --- /dev/null +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; +#endif + +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Manages telemetry collection and reporting for MSBuild. + /// This class provides a centralized way to initialize, configure, and manage telemetry sessions. + /// + /// + /// The TelemetryManager is a singleton that handles both standalone and integrated telemetry scenarios. + /// On .NET Framework, it integrates with Visual Studio telemetry services. + /// On .NET Core it provides a lightweight telemetry implementation through exposing an activity source. + /// + internal class TelemetryManager + { + /// + /// Lock object for thread-safe initialization and disposal. + /// + private static readonly LockType s_lock = new(); + + private static bool s_initialized; + private static bool s_disposed; + + private TelemetryManager() + { + } + + /// + /// Optional activity source for MSBuild or other telemetry usage. + /// + public MSBuildActivitySource? DefaultActivitySource { get; private set; } + + public static TelemetryManager Instance { get; } = new TelemetryManager(); + + /// + /// Initializes the telemetry manager with the specified configuration. + /// + /// + /// Indicates whether MSBuild is running in standalone mode (e.g., MSBuild.exe directly invoked) + /// versus integrated mode (e.g., running within Visual Studio or dotnet CLI). + /// When true, creates and manages its own telemetry session on .NET Framework. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public void Initialize(bool isStandalone) + { + lock (s_lock) + { + if (s_initialized) + { + return; + } + + s_initialized = true; + + if (IsOptOut()) + { + return; + } + + TryInitializeTelemetry(isStandalone); + } + } + + /// + /// Resets the TelemetryManager state for TESTING purposes. + /// + internal static void ResetForTest() + { + lock (s_lock) + { + s_initialized = false; + s_disposed = false; + Instance.DefaultActivitySource = null; + } + } + + /// + /// Initializes MSBuild telemetry. + /// This method is deliberately not inlined to ensure + /// the Telemetry related assemblies are only loaded when this method is called, + /// allowing the calling code to catch assembly loading exceptions. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void TryInitializeTelemetry(bool isStandalone) + { + try + { +#if NETFRAMEWORK + DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); +#else + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); +#endif + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) + { + // Microsoft.VisualStudio.Telemetry or System.Diagnostics.DiagnosticSource might not be available outside of VS or dotnet. + // This is expected in standalone application scenarios (when MSBuild.exe is invoked directly). + DefaultActivitySource = null; + } + } + + public void Dispose() + { + lock (s_lock) + { + if (s_disposed) + { + return; + } + +#if NETFRAMEWORK + try + { + DisposeVsTelemetry(); + } + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) + { + // Assembly was never loaded, nothing to dispose. + } +#endif + s_disposed = true; + } + } + + /// + /// Determines if the user has explicitly opted out of telemetry. + /// + internal static bool IsOptOut() => +#if NETFRAMEWORK + Traits.Instance.FrameworkTelemetryOptOut; +#else + Traits.Instance.SdkTelemetryOptOut; +#endif + +#if NETFRAMEWORK + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DisposeVsTelemetry() => VsTelemetryInitializer.Dispose(); +#endif + } + +#if NETFRAMEWORK + internal static class VsTelemetryInitializer + { + // Telemetry API key for Visual Studio telemetry service. + private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; + + // Store as object to avoid type reference at class load time + private static object? s_telemetrySession; + private static bool s_ownsSession = false; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static MSBuildActivitySource Initialize(bool isStandalone) + { + TelemetrySession session; + if (isStandalone) + { + session = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + TelemetryService.DefaultSession.UseVsIsOptedIn(); + TelemetryService.DefaultSession.Start(); + s_ownsSession = true; + } + else + { + session = TelemetryService.DefaultSession; + } + + s_telemetrySession = session; + return new MSBuildActivitySource(session); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Dispose() + { + if (s_ownsSession && s_telemetrySession is TelemetrySession session) + { + session.Dispose(); + } + + s_telemetrySession = null; + } + } +#endif +} diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs new file mode 100644 index 00000000000..f9f21374d1b --- /dev/null +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using System.Collections.Generic; +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Represents a Visual Studio telemetry activity that wraps a . + /// This class provides an implementation of for the VS Telemetry system, + /// allowing telemetry data to be collected and sent when running on .NET Framework. + /// + internal class VsTelemetryActivity : IActivity + { + private readonly TelemetryScope _scope; + private TelemetryResult _result = TelemetryResult.Success; + + private bool _disposed; + + public VsTelemetryActivity(TelemetryScope scope) + { + _scope = scope; + } + + public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + { + Dictionary? tags = dataHolder?.GetActivityProperties(); + + if (tags != null) + { + foreach (KeyValuePair tag in tags) + { + _ = SetTag(tag.Key, tag.Value); + } + } + + return this; + } + + public IActivity? SetTag(string key, object? value) + { + if (value != null) + { + _scope.EndEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{key}"] = new TelemetryComplexProperty(value); + } + + return this; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _scope.End(_result); + _disposed = true; + } + } +} + +#endif diff --git a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs index d643045ffe6..d127a280d54 100644 --- a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs +++ b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs @@ -8,7 +8,7 @@ namespace Microsoft.Build.Framework.Telemetry; internal class WorkerNodeTelemetryData : IWorkerNodeTelemetryData { - public WorkerNodeTelemetryData(Dictionary tasksExecutionData, Dictionary targetsExecutionData) + public WorkerNodeTelemetryData(Dictionary tasksExecutionData, Dictionary targetsExecutionData) { TasksExecutionData = tasksExecutionData; TargetsExecutionData = targetsExecutionData; @@ -18,42 +18,60 @@ public void Add(IWorkerNodeTelemetryData other) { foreach (var task in other.TasksExecutionData) { - AddTask(task.Key, task.Value.CumulativeExecutionTime, task.Value.ExecutionsCount, task.Value.TotalMemoryBytes); + AddTask(task.Key, task.Value.CumulativeExecutionTime, task.Value.ExecutionsCount, task.Value.TotalMemoryBytes, task.Value.TaskFactoryName, task.Value.TaskHostRuntime); } foreach (var target in other.TargetsExecutionData) { - AddTarget(target.Key, target.Value); + AddTarget(target.Key, target.Value.WasExecuted, target.Value.SkipReason); } } - public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTime, int executionsCount, long totalMemoryConsumption) + public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExecutionTime, int executionsCount, long totalMemoryConsumption, string? factoryName, string? taskHostRuntime) { TaskExecutionStats? taskExecutionStats; if (!TasksExecutionData.TryGetValue(task, out taskExecutionStats)) { - taskExecutionStats = new(cumulativeExectionTime, executionsCount, totalMemoryConsumption); + taskExecutionStats = new(cumulativeExecutionTime, executionsCount, totalMemoryConsumption, factoryName, taskHostRuntime); TasksExecutionData[task] = taskExecutionStats; } else { - taskExecutionStats.CumulativeExecutionTime += cumulativeExectionTime; + taskExecutionStats.CumulativeExecutionTime += cumulativeExecutionTime; taskExecutionStats.ExecutionsCount += executionsCount; taskExecutionStats.TotalMemoryBytes += totalMemoryConsumption; + taskExecutionStats.TaskFactoryName ??= factoryName; + taskExecutionStats.TaskHostRuntime ??= taskHostRuntime; } } - public void AddTarget(TaskOrTargetTelemetryKey target, bool wasExecuted) + public void AddTarget(TaskOrTargetTelemetryKey target, bool wasExecuted, TargetSkipReason skipReason = TargetSkipReason.None) { - TargetsExecutionData[target] = - // we just need to store if it was ever executed - wasExecuted || (TargetsExecutionData.TryGetValue(target, out bool wasAlreadyExecuted) && wasAlreadyExecuted); + if (TargetsExecutionData.TryGetValue(target, out var existingStats)) + { + // If the target was ever executed, mark it as executed + // Otherwise, keep the most informative skip reason (non-None preferred) + if (wasExecuted || existingStats.WasExecuted) + { + TargetsExecutionData[target] = TargetExecutionStats.Executed(); + } + else if (skipReason != TargetSkipReason.None) + { + TargetsExecutionData[target] = TargetExecutionStats.Skipped(skipReason); + } + // else keep existing stats + } + else + { + TargetsExecutionData[target] = wasExecuted + ? TargetExecutionStats.Executed() + : TargetExecutionStats.Skipped(skipReason); + } } - public WorkerNodeTelemetryData() - : this(new Dictionary(), new Dictionary()) - { } + public WorkerNodeTelemetryData() : this([], []) { } public Dictionary TasksExecutionData { get; } - public Dictionary TargetsExecutionData { get; } + + public Dictionary TargetsExecutionData { get; } } diff --git a/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs b/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs index 4eef343b196..38c2bb43910 100644 --- a/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs +++ b/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs @@ -27,13 +27,16 @@ internal override void WriteToStream(BinaryWriter writer) writer.Write(entry.Value.CumulativeExecutionTime.Ticks); writer.Write(entry.Value.ExecutionsCount); writer.Write(entry.Value.TotalMemoryBytes); + writer.Write(entry.Value.TaskFactoryName ?? string.Empty); + writer.Write(entry.Value.TaskHostRuntime ?? string.Empty); } writer.Write7BitEncodedInt(WorkerNodeTelemetryData.TargetsExecutionData.Count); - foreach (KeyValuePair entry in WorkerNodeTelemetryData.TargetsExecutionData) + foreach (KeyValuePair entry in WorkerNodeTelemetryData.TargetsExecutionData) { WriteToStream(writer, entry.Key); - writer.Write(entry.Value); + writer.Write(entry.Value.WasExecuted); + writer.Write((int)entry.Value.SkipReason); } } @@ -43,18 +46,31 @@ internal override void CreateFromStream(BinaryReader reader, int version) Dictionary tasksExecutionData = new(); for (int i = 0; i < count; i++) { - tasksExecutionData.Add(ReadFromStream(reader), + var key = ReadFromStream(reader); + var cumulativeExecutionTime = TimeSpan.FromTicks(reader.ReadInt64()); + var executionsCount = reader.ReadInt32(); + var totalMemoryBytes = reader.ReadInt64(); + var taskFactoryName = reader.ReadString(); + var taskHostRuntime = reader.ReadString(); + + tasksExecutionData.Add( + key, new TaskExecutionStats( - TimeSpan.FromTicks(reader.ReadInt64()), - reader.ReadInt32(), - reader.ReadInt64())); + cumulativeExecutionTime, + executionsCount, + totalMemoryBytes, + string.IsNullOrEmpty(taskFactoryName) ? null : taskFactoryName, + string.IsNullOrEmpty(taskHostRuntime) ? null : taskHostRuntime)); } count = reader.Read7BitEncodedInt(); - Dictionary targetsExecutionData = new(); + Dictionary targetsExecutionData = new(); for (int i = 0; i < count; i++) { - targetsExecutionData.Add(ReadFromStream(reader), reader.ReadBoolean()); + var key = ReadFromStream(reader); + var wasExecuted = reader.ReadBoolean(); + var skipReason = (TargetSkipReason)reader.ReadInt32(); + targetsExecutionData.Add(key, new TargetExecutionStats(wasExecuted, skipReason)); } WorkerNodeTelemetryData = new WorkerNodeTelemetryData(tasksExecutionData, targetsExecutionData); diff --git a/src/Framework/TelemetryEventArgs.cs b/src/Framework/TelemetryEventArgs.cs index d3d57e9c5e5..645a72526d3 100644 --- a/src/Framework/TelemetryEventArgs.cs +++ b/src/Framework/TelemetryEventArgs.cs @@ -43,6 +43,7 @@ internal override void WriteToStream(BinaryWriter writer) writer.WriteOptionalString(kvp.Value); } } + internal override void CreateFromStream(BinaryReader reader, int version) { base.CreateFromStream(reader, version); diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 8cbf21feef1..f14208e145a 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -135,6 +135,28 @@ public Traits() /// public const string UseMSBuildServerEnvVarName = "MSBUILDUSESERVER"; + /// + /// Name of environment variable for logging arguments (e.g., -bl, -check). + /// + public const string MSBuildLoggingArgsEnvVarName = "MSBUILD_LOGGING_ARGS"; + + /// + /// Name of environment variable that controls the logging level for diagnostic messages + /// emitted when processing the MSBUILD_LOGGING_ARGS environment variable. + /// Set to "message" to emit as low-importance build messages instead of console warnings. + /// + public const string MSBuildLoggingArgsLevelEnvVarName = "MSBUILD_LOGGING_ARGS_LEVEL"; + + /// + /// Value of the MSBUILD_LOGGING_ARGS environment variable. + /// + public static string? MSBuildLoggingArgs => Environment.GetEnvironmentVariable(MSBuildLoggingArgsEnvVarName); + + /// + /// Gets if the logging level for MSBUILD_LOGGING_ARGS diagnostic is message. + /// + public readonly bool EmitLogsAsMessage = string.Equals(Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName), "message", StringComparison.OrdinalIgnoreCase); + public readonly bool DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); public readonly bool DebugScheduler; public readonly bool DebugNodeCommunication; @@ -147,6 +169,11 @@ public Traits() /// public readonly bool ForceTaskFactoryOutOfProc = Environment.GetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC") == "1"; + /// + /// Make Console use default encoding in the system. It opts out automatic console encoding UTF-8. + /// + public readonly bool ConsoleUseDefaultEncoding = Environment.GetEnvironmentVariable("MSBUILD_CONSOLE_USE_DEFAULT_ENCODING") == "1" || Environment.GetEnvironmentVariable("DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING") == "1"; + /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. /// mirroring @@ -154,14 +181,12 @@ public Traits() /// public bool SdkTelemetryOptOut = IsEnvVarOneOrTrue("DOTNET_CLI_TELEMETRY_OPTOUT"); public bool FrameworkTelemetryOptOut = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTOUT"); - public double? TelemetrySampleRateOverride = ParseDoubleFromEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE"); public bool ExcludeTasksDetailsFromTelemetry = IsEnvVarOneOrTrue("MSBUILDTELEMETRYEXCLUDETASKSDETAILS"); public bool FlushNodesTelemetryIntoConsole = IsEnvVarOneOrTrue("MSBUILDFLUSHNODESTELEMETRYINTOCONSOLE"); public bool EnableTargetOutputLogging = IsEnvVarOneOrTrue("MSBUILDTARGETOUTPUTLOGGING"); // for VS17.14 - public readonly bool TelemetryOptIn = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTIN"); public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); public static void UpdateFromEnvironment() @@ -180,19 +205,6 @@ private static int ParseIntFromEnvironmentVariableOrDefault(string environmentVa : defaultValue; } - /// - /// Parse a double from an environment variable with invariant culture. - /// - private static double? ParseDoubleFromEnvironmentVariable(string environmentVariable) - { - return double.TryParse(Environment.GetEnvironmentVariable(environmentVariable), - NumberStyles.Float, - CultureInfo.InvariantCulture, - out double result) - ? result - : null; - } - internal static bool IsEnvVarOneOrTrue(string name) { string? value = Environment.GetEnvironmentVariable(name); diff --git a/src/MSBuild.UnitTests/CommandLineParserTests.cs b/src/MSBuild.UnitTests/CommandLineParserTests.cs new file mode 100644 index 00000000000..cb5280d913c --- /dev/null +++ b/src/MSBuild.UnitTests/CommandLineParserTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.CommandLine.Experimental; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.CommandLine.UnitTests +{ + public class CommandLineParserTests + { + [Fact] + public void ParseReturnsInstance() + { + CommandLineParser parser = new CommandLineParser(); + CommandLineSwitchesAccessor result = parser.Parse(["/targets:targets.txt"]); // first parameter must be the executable name + + result.Targets.ShouldNotBeNull(); + result.Targets.ShouldBe(["targets.txt"]); + } + + [Fact] + public void ParseThrowsException() + { + CommandLineParser parser = new CommandLineParser(); + + Should.Throw(() => + { + // first parameter must be the executable name + parser.Parse(["tempproject.proj", "tempproject.proj"]); + }); + } + } +} diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index a2de7a8fb1c..7cbf86ce493 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Resources; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Construction; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -622,7 +623,9 @@ public void FeatureAvailibilitySwitchIdentificationTest(string switchName) public void TargetsSwitchParameter() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets:targets.txt" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets:targets.txt"], switches); switches.HaveErrors().ShouldBeFalse(); switches[CommandLineSwitches.ParameterizedSwitch.Targets].ShouldBe(new[] { "targets.txt" }); @@ -632,7 +635,9 @@ public void TargetsSwitchParameter() public void TargetsSwitchDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets /targets" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets /targets"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -709,8 +714,9 @@ public void LowPrioritySwitchIdentificationTests(string lowpriority) public void GraphBuildSwitchCanHaveParameters() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph", "/graph:true; NoBuild ;; ;", "/graph:foo" }, switches); + parser.GatherCommandLineSwitches(["/graph", "/graph:true; NoBuild ;; ;", "/graph:foo"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(new[] { "true", " NoBuild ", " ", "foo" }); @@ -721,8 +727,9 @@ public void GraphBuildSwitchCanHaveParameters() public void GraphBuildSwitchCanBeParameterless() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph" }, switches); + parser.GatherCommandLineSwitches(["/graph"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(Array.Empty()); @@ -733,8 +740,9 @@ public void GraphBuildSwitchCanBeParameterless() public void InputResultsCachesSupportsMultipleOccurrence() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/irc", "/irc:a;b", "/irc:c;d" }, switches); + parser.GatherCommandLineSwitches(["/irc", "/irc:a;b", "/irc:c;d"], switches); switches[CommandLineSwitches.ParameterizedSwitch.InputResultsCaches].ShouldBe(new[] { null, "a", "b", "c", "d" }); @@ -745,8 +753,9 @@ public void InputResultsCachesSupportsMultipleOccurrence() public void OutputResultsCache() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a" }, switches); + parser.GatherCommandLineSwitches(["/orc:a"], switches); switches[CommandLineSwitches.ParameterizedSwitch.OutputResultsCache].ShouldBe(new[] { "a" }); @@ -757,8 +766,9 @@ public void OutputResultsCache() public void OutputResultsCachesDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a", "/orc:b" }, switches); + parser.GatherCommandLineSwitches(["/orc:a", "/orc:b"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -1288,8 +1298,9 @@ public void ExtractAnyLoggerParameterPickLast() public void ProcessWarnAsErrorSwitchNotSpecified() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "" }), commandLineSwitches); + parser.GatherCommandLineSwitches([""], commandLineSwitches); Assert.Null(MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches)); } @@ -1303,16 +1314,17 @@ public void ProcessWarnAsErrorSwitchWithCodes() ISet expectedWarningsAsErrors = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnaserror: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnaserror:A,b,C", // Repeats of different case "\"/warnaserror:, ,,\"", // Empty items "/err:D,d;E,e", // A different source with new items and uses the short form "/warnaserror:a", // A different source with a single duplicate "/warnaserror:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1328,12 +1340,13 @@ public void ProcessWarnAsErrorSwitchWithCodes() public void ProcessWarnAsErrorSwitchEmptySwitchClearsSet() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1351,13 +1364,14 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() ISet expectedWarningsAsErors = new HashSet(StringComparer.OrdinalIgnoreCase) { "e", "f", "g" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", "/warnaserror:e;f;g", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1373,8 +1387,9 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() public void ProcessWarnAsErrorSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/warnaserror"], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1390,10 +1405,11 @@ public void ProcessWarnAsErrorSwitchEmpty() public void ProcessWarnAsMessageSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); // Set "expanded" content to match the placeholder so the verify can use the exact resource string as "expected." string command = "{0}"; - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnasmessage" }), commandLineSwitches, command); + parser.GatherCommandLineSwitches(["/warnasmessage"], commandLineSwitches, command); VerifySwitchError(commandLineSwitches, "/warnasmessage", AssemblyResources.GetString("MissingWarnAsMessageParameterError")); } @@ -1410,13 +1426,15 @@ public void ProcessEnvironmentVariableSwitch() env.SetEnvironmentVariable("ENVIRONMENTVARIABLE", string.Empty); CommandLineSwitches commandLineSwitches = new(); + CommandLineParser parser = new CommandLineParser(); + string fullCommandLine = "msbuild validProject.csproj %ENVIRONMENTVARIABLE%"; - MSBuildApp.GatherCommandLineSwitches(new List() { "validProject.csproj", "%ENVIRONMENTVARIABLE%" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["validProject.csproj", "%ENVIRONMENTVARIABLE%"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); commandLineSwitches = new(); fullCommandLine = "msbuild %ENVIRONMENTVARIABLE% validProject.csproj"; - MSBuildApp.GatherCommandLineSwitches(new List() { "%ENVIRONMENTVARIABLE%", "validProject.csproj" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["%ENVIRONMENTVARIABLE%", "validProject.csproj"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); } } @@ -1430,16 +1448,17 @@ public void ProcessWarnAsMessageSwitchWithCodes() ISet expectedWarningsAsMessages = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnasmessage: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnasmessage:A,b,C", // Repeats of different case "\"/warnasmessage:, ,,\"", // Empty items "/nowarn:D,d;E,e", // A different source with new items and uses the short form "/warnasmessage:a", // A different source with a single duplicate "/warnasmessage:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsMessages = MSBuildApp.ProcessWarnAsMessageSwitch(commandLineSwitches); @@ -1455,8 +1474,9 @@ public void ProcessWarnAsMessageSwitchWithCodes() public void ProcessProfileEvaluationEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/profileevaluation" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/profileevaluation"], commandLineSwitches); commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ProfileEvaluation][0].ShouldBe("no-file"); } @@ -1548,11 +1568,7 @@ public void ProcessInvalidTargetSwitch() using TestEnvironment testEnvironment = TestEnvironment.Create(); string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"msbuild.exe " + project + " /t:foo.bar").ShouldBe(MSBuildApp.ExitType.SwitchError); -#else - MSBuildApp.Execute(new[] { @"msbuild.exe", project, "/t:foo.bar" }).ShouldBe(MSBuildApp.ExitType.SwitchError); -#endif + MSBuildApp.Execute([@"msbuild.exe", project, "/t:foo.bar"]).ShouldBe(MSBuildApp.ExitType.SwitchError); } /// diff --git a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs index 7a224860a2f..82fa56a588c 100644 --- a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs +++ b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs @@ -52,7 +52,7 @@ public void VerifyInvalidProjectSchema() "); string quotedProjectFilename = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFilename + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFilename, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); } finally { @@ -95,7 +95,7 @@ public void VerifyInvalidSchemaItself1() "); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -155,7 +155,7 @@ public void VerifyInvalidSchemaItself2() string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -203,7 +203,7 @@ public void VerifyValidProjectSchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( @@ -256,7 +256,7 @@ public void VerifyInvalidImportNotCaughtBySchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( diff --git a/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs new file mode 100644 index 00000000000..af764f80e36 --- /dev/null +++ b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.CommandLine.Experimental; +using Microsoft.Build.Execution; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for MSBUILD_LOGGING_ARGS environment variable functionality. + /// + public class XMakeBinlogSwitchTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestEnvironment _env; + + public XMakeBinlogSwitchTests(ITestOutputHelper output) + { + _output = output; + _env = TestEnvironment.Create(output); + } + + public void Dispose() => _env.Dispose(); + + /// + /// Test that MSBUILD_LOGGING_ARGS with -bl creates a binary log. + /// + [Fact] + public void LoggingArgsEnvVarWithBinaryLogger() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS with multiple -bl switches creates multiple binary logs. + /// + [Fact] + public void LoggingArgsEnvVarWithMultipleBinaryLoggers() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath1 = Path.Combine(directory.Path, "test1.binlog"); + string binlogPath2 = Path.Combine(directory.Path, "test2.binlog"); + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath1} -bl:{binlogPath2}"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + File.Exists(binlogPath1).ShouldBeTrue($"First binary log should have been created at {binlogPath1}"); + File.Exists(binlogPath2).ShouldBeTrue($"Second binary log should have been created at {binlogPath2}"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS with {} placeholder generates unique filenames. + /// + [Fact] + public void LoggingArgsEnvVarWithWildcardPlaceholder() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + // Use {} placeholder for unique filename generation + string binlogPattern = Path.Combine(directory.Path, "build-{}.binlog"); + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPattern}"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Find the generated binlog file (should have unique characters instead of {}) + string[] binlogFiles = Directory.GetFiles(directory.Path, "build-*.binlog"); + binlogFiles.Length.ShouldBe(1, $"Expected exactly one binlog file to be created in {directory.Path}"); + + // The filename should not contain {} - it should have been replaced with unique characters + binlogFiles[0].ShouldNotContain("{}"); + binlogFiles[0].ShouldContain("build-"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS with multiple {} placeholders generates unique filenames with each placeholder replaced. + /// + [Fact] + public void LoggingArgsEnvVarWithMultipleWildcardPlaceholders() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + // Use multiple {} placeholders for unique filename generation + string binlogPattern = Path.Combine(directory.Path, "build-{}-test-{}.binlog"); + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPattern}"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Find the generated binlog file (should have unique characters instead of {}) + string[] binlogFiles = Directory.GetFiles(directory.Path, "build-*-test-*.binlog"); + binlogFiles.Length.ShouldBe(1, $"Expected exactly one binlog file to be created in {directory.Path}"); + + // The filename should not contain {} - both placeholders should have been replaced + binlogFiles[0].ShouldNotContain("{}"); + binlogFiles[0].ShouldContain("build-"); + binlogFiles[0].ShouldContain("-test-"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS ignores unsupported arguments and continues with valid ones. + /// + [Fact] + public void LoggingArgsEnvVarIgnoresUnsupportedArguments() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + // Set env var with mixed valid and invalid arguments + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath} -maxcpucount:4 -verbosity:detailed"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Binary log should still be created (valid argument) + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}"); + + // Warning should appear for invalid arguments + output.ShouldContain("MSB1070"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS works with /noautoresponse. + /// + [Fact] + public void LoggingArgsEnvVarWorksWithNoAutoResponse() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}"); + + // Use /noautoresponse - MSBUILD_LOGGING_ARGS should still work + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\" /noautoresponse", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created even with /noautoresponse"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS_LEVEL=message emits diagnostics as messages instead of warnings. + /// + [Fact] + public void LoggingArgsEnvVarLevelMessageSuppressesWarnings() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4"); + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS_LEVEL", "message"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldNotContain("MSB1070"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS emits warnings by default when MSBUILD_LOGGING_ARGS_LEVEL is not set. + /// + [Fact] + public void LoggingArgsEnvVarDefaultLevelEmitsWarnings() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + // Set env var with invalid argument, but do NOT set MSBUILD_LOGGING_ARGS_LEVEL + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Warning SHOULD appear when level is not set (default behavior) + output.ShouldContain("MSB1070"); + } + + /// + /// Test that empty or whitespace MSBUILD_LOGGING_ARGS is ignored. + /// + [Fact] + public void LoggingArgsEnvVarEmptyIsIgnored() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", " "); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + } + + /// + /// Test that -check switch is allowed in MSBUILD_LOGGING_ARGS. + /// + [Fact] + public void LoggingArgsEnvVarAllowsCheckSwitch() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-check"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldNotContain("MSB1070"); + } + + /// + /// Test that only logging-related switches are allowed. + /// + [Theory] + [InlineData("-bl")] + [InlineData("-bl:test.binlog")] + [InlineData("-binarylogger")] + [InlineData("-binarylogger:test.binlog")] + [InlineData("/bl")] + [InlineData("/bl:test.binlog")] + [InlineData("--bl")] + [InlineData("-check")] + [InlineData("/check")] + public void LoggingArgsEnvVarAllowedSwitches(string switchArg) + { + CommandLineParser parser = new(); + _ = _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg); + + CommandLineSwitches switches = new(); + List deferredBuildMessages = new(); + parser.GatherLoggingArgsEnvironmentVariableSwitches(ref switches, deferredBuildMessages, "test"); + + switches.HaveErrors().ShouldBeFalse($"Switch {switchArg} should be allowed"); + } + + /// + /// Test that non-logging switches are rejected. + /// + [Theory] + [InlineData("-property:A=1")] + [InlineData("-target:Build")] + [InlineData("-verbosity:detailed")] + [InlineData("-maxcpucount:4")] + [InlineData("/p:A=1")] + [InlineData("-restore")] + [InlineData("-nologo")] + public void LoggingArgsEnvVarDisallowedSwitches(string switchArg) + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldContain("MSB1070"); + } + } +} diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index f4eafd483a6..8cdd6c467a9 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.Build.Logging; @@ -96,11 +97,9 @@ public XMakeAppTests(ITestOutputHelper output) public void GatherCommandLineSwitchesTwoProperties() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/p:a=b", "/p:c=d" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/p:a=b", "/p:c=d"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -111,13 +110,9 @@ public void GatherCommandLineSwitchesTwoProperties() public void GatherCommandLineSwitchesAnyDash() { var switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List { - "-p:a=b", - "--p:maxcpucount=8" - }; - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["-p:a=b", "--p:maxcpucount=8"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -128,11 +123,9 @@ public void GatherCommandLineSwitchesAnyDash() public void GatherCommandLineSwitchesMaxCpuCountWithArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:2" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:2"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[0].ShouldBe("2"); @@ -145,11 +138,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:3", "/m" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:3", "/m"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[1].ShouldBe(Convert.ToString(NativeMethodsShared.GetLogicalCoreCount())); @@ -165,11 +156,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgumentButWithColon() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters.Length.ShouldBe(0); @@ -459,44 +448,44 @@ public void ExtractSwitchParametersTest() { string commandLineArg = "\"/p:foo=\"bar"; string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "/p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); // this test is totally unreal -- we'd never attempt to extract switch parameters if the leading character is not a // switch indicator (either '-' or '/') -- here the leading character is a double-quote commandLineArg = "\"\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "/p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -505,37 +494,37 @@ public void ExtractSwitchParametersTestDoubleDash() { var commandLineArg = "\"--p:foo=\"bar"; var unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "--p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "--p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -548,11 +537,11 @@ public void GetLengthOfSwitchIndicatorTest() var commandLineSwitchWithNoneOrIncorrectIndicator = "zSwitch"; - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); } [Theory] @@ -562,12 +551,7 @@ public void GetLengthOfSwitchIndicatorTest() [InlineData(@"/h")] public void Help(string indicator) { - MSBuildApp.Execute( -#if FEATURE_GET_COMMANDLINE - @$"c:\bin\msbuild.exe {indicator} ") -#else - new[] { @"c:\bin\msbuild.exe", indicator }) -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", indicator]) .ShouldBe(MSBuildApp.ExitType.Success); } @@ -660,19 +644,11 @@ public void VersionSwitchDisableChangeWave() public void ErrorCommandLine() { string oldValueForMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe -junk").ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(@"msbuild.exe -t").ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(@"msbuild.exe @bogus.rsp").ShouldBe(MSBuildApp.ExitType.InitializationError); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", "-junk" }).ShouldBe(MSBuildApp.ExitType.SwitchError); - MSBuildApp.Execute(new[] { @"msbuild.exe", "-t" }).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"c:\bin\msbuild.exe", "-junk"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "-t"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "@bogus.rsp"]).ShouldBe(MSBuildApp.ExitType.InitializationError); - MSBuildApp.Execute(new[] { @"msbuild.exe", "@bogus.rsp" }).ShouldBe(MSBuildApp.ExitType.InitializationError); -#endif Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", oldValueForMSBuildLoadMicrosoftTargetsReadOnly); } @@ -888,6 +864,38 @@ public void GetStarOutputsToFileIfRequested(string extraSwitch, string result) result.ShouldContain("MSB1068"); } + /// + /// Regression test for issue where getTargetResult/getItem would throw an unhandled exception + /// when the item spec contained illegal path characters (e.g. compiler command line flags). + /// + [Theory] + [InlineData("-getTargetResult:GetCompileCommands", "\"Result\": \"Success\"")] + [InlineData("-getItem:CompileCommands", "\"Identity\":")] + public void GetTargetResultWithIllegalPathCharacters(string extraSwitch, string expectedContent) + { + using TestEnvironment env = TestEnvironment.Create(); + // Create a project that mimics the ClangTidy target - it outputs items with illegal path characters + // (compiler command line flags) as the item spec. + TransientTestFile project = env.CreateFile("testProject.csproj", @" + + + + + + + + + + + +"); + string results = RunnerUtilities.ExecMSBuild($" {project.Path} /t:GetCompileCommands {extraSwitch}", out bool success); + // The build should succeed instead of throwing an unhandled exception + success.ShouldBeTrue(results); + // The output should contain the expected content + results.ShouldContain(expectedContent); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -1153,11 +1161,7 @@ public void TestEnvironmentTest() sw.WriteLine(projectString); } // Should pass -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", quotedProjectFileName }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", quotedProjectFileName]).ShouldBe(MSBuildApp.ExitType.Success); } finally { @@ -1190,21 +1194,15 @@ public void MSBuildEngineLogger() { sw.WriteLine(projectString); } -#if FEATURE_GET_COMMANDLINE - // Should pass - MSBuildApp.Execute(@$"c:\bin\msbuild.exe /logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}"" /verbosity:detailed " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); - -#else // Should pass - MSBuildApp.Execute( - new[] - { + MSBuildApp + .Execute([ NativeMethodsShared.IsWindows ? @"c:\bin\msbuild.exe" : "/msbuild.exe", @$"/logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}""", "/verbosity:detailed", - quotedProjectFileName - }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + quotedProjectFileName]) + .ShouldBe(MSBuildApp.ExitType.Success); + File.Exists(logFile).ShouldBeTrue(); var logFileContents = File.ReadAllText(logFile); @@ -2262,6 +2260,44 @@ public void TestProcessFileLoggerSwitch5() distributedLoggerRecords.Count.ShouldBe(0); // "Expected no distributed loggers to be attached" loggers.Count.ShouldBe(0); // "Expected no central loggers to be attached" } + + /// + /// Verify that DistributedLoggerRecords with null CentralLogger don't cause exceptions when creating ProjectCollection + /// This is a regression test for the issue where -dfl flag caused MSB1025 error due to null logger not being filtered. + /// + [Fact] + public void TestNullCentralLoggerInDistributedLoggerRecord() + { + // Simulate the scenario when using -dfl flag + // ProcessDistributedFileLogger creates a DistributedLoggerRecord with null CentralLogger + var distributedLoggerRecords = new List(); + bool distributedFileLogger = true; + string[] fileLoggerParameters = null; + + MSBuildApp.ProcessDistributedFileLogger( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords); + + // Verify that we have a distributed logger record with null central logger + distributedLoggerRecords.Count.ShouldBe(1); + distributedLoggerRecords[0].CentralLogger.ShouldBeNull(); + + // This should not throw ArgumentNullException when creating ProjectCollection + // The fix filters out null central loggers from the evaluationLoggers array + var loggers = Array.Empty(); + Should.NotThrow(() => + { + using var projectCollection = new ProjectCollection( + new Dictionary(), + loggers: [.. loggers, .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not null)], + remoteLoggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: true); + }); + } #endregion #region ProcessConsoleLoggerSwitches @@ -2682,6 +2718,116 @@ public void BinaryLogContainsImportedFiles() archive.Entries.ShouldContain(e => e.FullName.EndsWith(".proj", StringComparison.OrdinalIgnoreCase), 2); } + [Fact] + public void MultipleBinaryLogsCreatesMultipleFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + string binLog3 = Path.Combine(binLogLocation, "3.binlog"); + + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog3}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify all three binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + File.Exists(binLog3).ShouldBeTrue("Third binlog file should exist"); + + // Verify all files have content (are not empty) + new FileInfo(binLog1).Length.ShouldBeGreaterThan(0, "First binlog should not be empty"); + new FileInfo(binLog2).Length.ShouldBeGreaterThan(0, "Second binlog should not be empty"); + new FileInfo(binLog3).Length.ShouldBeGreaterThan(0, "Third binlog should not be empty"); + + // Verify all files are identical (have the same content) + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + byte[] file3Bytes = File.ReadAllBytes(binLog3); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("First and second binlog should be identical"); + file1Bytes.SequenceEqual(file3Bytes).ShouldBeTrue("First and third binlog should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDuplicatesCreateDistinctFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + + // Specify binLog1 twice - should only create two distinct files + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog1}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify both files are identical + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("Binlog files should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDifferentConfigurationsCreatesSeparateLoggers() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + + "); + + _env.CreateFile("Imported.proj", @" + + + Value + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "with-imports.binlog"); + string binLog2 = Path.Combine(binLogLocation, "no-imports.binlog"); + + // One with default imports, one with ProjectImports=None + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2};ProjectImports=None\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify files are different sizes (one has imports embedded, one doesn't) + long size1 = new FileInfo(binLog1).Length; + long size2 = new FileInfo(binLog2).Length; + + size1.ShouldBeGreaterThan(size2, "Binlog with imports should be larger than one without"); + } + [Theory] [InlineData("-warnaserror", "", "", false)] [InlineData("-warnaserror -warnnotaserror:FOR123", "", "", true)] @@ -2935,6 +3081,20 @@ public void TasksGetAssemblyLoadContexts() #endif + [Fact] + public void ThrowsWhenMaxCpuCountTooLargeForMultiThreadedAndForceAllTasksOutOfProc() + { + string projectContent = """ + + + """; + using TestEnvironment testEnvironment = TestEnvironment.Create(); + testEnvironment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; + + MSBuildApp.Execute([@"c:\bin\msbuild.exe", project, "/m:257 /mt"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + } + private string CopyMSBuild() { string dest = null; diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index f93e8a6db00..c0407dd5a2d 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -11,6 +11,9 @@ [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +// Grant the dotnet CLI access to our command-line parsing logic, which it uses to parse MSBuild arguments. +[assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, // so that we don't run into known security issues with loading libraries from unsafe locations [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs new file mode 100644 index 00000000000..8405bf79fc6 --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -0,0 +1,741 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using static Microsoft.Build.CommandLine.Experimental.CommandLineSwitches; +using static Microsoft.Build.Execution.BuildManager; + +#nullable disable + +namespace Microsoft.Build.CommandLine.Experimental +{ + internal class CommandLineParser + { + /// + /// String replacement pattern to support paths in response files. + /// + private const string responseFilePathReplacement = "%MSBuildThisFileDirectory%"; + + /// + /// The name of an auto-response file to search for in the project directory and above. + /// + private const string directoryResponseFileName = "Directory.Build.rsp"; + + /// + /// The name of the auto-response file. + /// + private const string autoResponseFileName = "MSBuild.rsp"; + + /// + /// Used to keep track of response files to prevent them from + /// being included multiple times (or even recursively). + /// + private List includedResponseFiles; + + internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); + + /// + /// Parses the provided command-line arguments into a . + /// + /// + /// The command-line arguments excluding the executable path. + /// + /// + /// A containing the effective set of switches after combining + /// switches from response files (including any auto-response file) with switches from the command line, + /// where command-line switches take precedence. + /// + /// + /// Thrown when invalid switch syntax or values are encountered while parsing the command line or response files. + /// + public CommandLineSwitchesAccessor Parse(IEnumerable commandLineArgs) + { + List args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..commandLineArgs]; + List deferredBuildMessages = []; + + GatherAllSwitches( + args, + deferredBuildMessages, + out CommandLineSwitches responseFileSwitches, + out CommandLineSwitches commandLineSwitches, + out string fullCommandLine, + out _); + + CommandLineSwitches result = new(); + result.Append(responseFileSwitches, fullCommandLine); // lowest precedence + result.Append(commandLineSwitches, fullCommandLine); + + result.ThrowErrors(); + + return new CommandLineSwitchesAccessor(result); + } + + /// + /// Gets all specified switches, from the command line, as well as all + /// response files, including the auto-response file. + /// + /// + /// + /// + /// + /// + /// Combined bag of switches. + internal void GatherAllSwitches( + IEnumerable commandLineArgs, + List deferredBuildMessages, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine, + out string exeName) + { + ResetGatheringSwitchesState(); + + // discard the first piece, because that's the path to the executable -- the rest are args + commandLineArgs = commandLineArgs.Skip(1); + + exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + +#if USE_MSBUILD_DLL_EXTN + var msbuildExtn = ".dll"; +#else + var msbuildExtn = ".exe"; +#endif + if (!exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) + { + exeName += msbuildExtn; + } + + fullCommandLine = $"'{string.Join(" ", commandLineArgs)}'"; + + // parse the command line, and flag syntax errors and obvious switch errors + switchesNotFromAutoResponseFile = new CommandLineSwitches(); + GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile, fullCommandLine); + + // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the + // switches on the command line + switchesFromAutoResponseFile = new CommandLineSwitches(); + if (!switchesNotFromAutoResponseFile[ParameterlessSwitch.NoAutoResponse]) + { + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + GatherAutoResponseFileSwitches(exePath, switchesFromAutoResponseFile, fullCommandLine); + } + + CommandLineSwitches switchesFromEnvironmentVariable = new(); + GatherLoggingArgsEnvironmentVariableSwitches(ref switchesFromEnvironmentVariable, deferredBuildMessages, fullCommandLine); + switchesNotFromAutoResponseFile.Append(switchesFromEnvironmentVariable, fullCommandLine); + } + + /// + /// Gathers and validates logging switches from the MSBUILD_LOGGING_ARGS environment variable. + /// Only -bl and -check switches are allowed. All other switches are logged as warnings and ignored. + /// + internal void GatherLoggingArgsEnvironmentVariableSwitches( + ref CommandLineSwitches switches, + List deferredBuildMessages, + string commandLine) + { + if (string.IsNullOrWhiteSpace(Traits.MSBuildLoggingArgs)) + { + return; + } + + DeferredBuildMessageSeverity messageSeverity = Traits.Instance.EmitLogsAsMessage ? DeferredBuildMessageSeverity.Message : DeferredBuildMessageSeverity.Warning; + + try + { + List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); + + List validArgs = new(envVarArgs.Count); + List invalidArgs = null; + + foreach (string arg in envVarArgs) + { + string unquotedArg = QuotingUtilities.Unquote(arg); + if (string.IsNullOrWhiteSpace(unquotedArg)) + { + continue; + } + + if (IsAllowedLoggingArg(unquotedArg)) + { + validArgs.Add(arg); + } + else + { + invalidArgs ??= []; + invalidArgs.Add(unquotedArg); + } + } + + if (invalidArgs != null) + { + foreach (string invalidArg in invalidArgs) + { + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg); + deferredBuildMessages.Add(new DeferredBuildMessage(message, warningCode, messageSeverity)); + } + } + + if (validArgs.Count > 0) + { + deferredBuildMessages.Add(new DeferredBuildMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), MessageImportance.Low)); + GatherCommandLineSwitches(validArgs, switches, commandLine); + } + } + catch (Exception ex) + { + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()); + deferredBuildMessages.Add(new DeferredBuildMessage(message, errorCode, messageSeverity)); + } + } + + /// + /// Checks if the argument is an allowed logging argument (-bl or -check). + /// + /// The unquoted argument to check. + /// True if the argument is allowed, false otherwise. + private bool IsAllowedLoggingArg(string arg) + { + if (!ValidateSwitchIndicatorInUnquotedArgument(arg)) + { + return false; + } + + ReadOnlySpan switchPart = arg.AsSpan(GetLengthOfSwitchIndicator(arg)); + + // Extract switch name (before any ':' parameter indicator) + int colonIndex = switchPart.IndexOf(':'); + ReadOnlySpan switchNameSpan = colonIndex >= 0 ? switchPart.Slice(0, colonIndex) : switchPart; + string switchName = switchNameSpan.ToString(); + + return IsParameterizedSwitch( + switchName, + out ParameterizedSwitch paramSwitch, + out _, + out _, + out _, + out _, + out _) && (paramSwitch == ParameterizedSwitch.BinaryLogger || paramSwitch == ParameterizedSwitch.Check); + } + + /// + /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and + /// flags syntax errors, and other obvious switch errors. + /// + /// + /// Internal for unit testing only. + /// + internal void GatherCommandLineSwitches(IEnumerable commandLineArgs, CommandLineSwitches commandLineSwitches, string commandLine = "") + { + foreach (string commandLineArg in commandLineArgs) + { + string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); + + if (unquotedCommandLineArg.Length > 0) + { + // response file switch starts with @ + if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) + { + GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches, commandLine); + } + else + { + string switchName; + string switchParameters; + + // all switches should start with - or / or -- unless a project is being specified + if (!ValidateSwitchIndicatorInUnquotedArgument(unquotedCommandLineArg) || FileUtilities.LooksLikeUnixFilePath(unquotedCommandLineArg)) + { + switchName = null; + // add a (fake) parameter indicator for later parsing + switchParameters = $":{commandLineArg}"; + } + else + { + // check if switch has parameters (look for the : parameter indicator) + int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); + + // get the length of the beginning sequence considered as a switch indicator (- or / or --) + int switchIndicatorsLength = GetLengthOfSwitchIndicator(unquotedCommandLineArg); + + // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the + // leading - or / or --) and the parameter indicator (if the switch has parameters); the parameters (if any) + // follow the parameter indicator + if (switchParameterIndicator == -1) + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength); + switchParameters = string.Empty; + } + else + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength, switchParameterIndicator - switchIndicatorsLength); + switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator, switchIndicatorsLength); + } + } + + // Special case: for the switches "/m" (or "/maxCpuCount") and "/bl" (or "/binarylogger") we wish to pretend we saw a default argument + // This allows a subsequent /m:n on the command line to override it. + // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. + // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu + // for backwards compatibility. + if (string.IsNullOrEmpty(switchParameters)) + { + if (string.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) + { + int numberOfCpus = NativeMethodsShared.GetLogicalCoreCount(); + switchParameters = $":{numberOfCpus}"; + } + else if (string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase)) + { + // we have to specify at least one parameter otherwise it's impossible to distinguish the situation + // where /bl is not specified at all vs. where /bl is specified without the file name. + switchParameters = ":msbuild.binlog"; + } + else if (string.Equals(switchName, "prof", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "profileevaluation", StringComparison.OrdinalIgnoreCase)) + { + switchParameters = ":no-file"; + } + } + + if (CommandLineSwitches.IsParameterlessSwitch(switchName, out var parameterlessSwitch, out var duplicateSwitchErrorMessage)) + { + GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out var parameterizedSwitch, out duplicateSwitchErrorMessage, out var multipleParametersAllowed, out var missingParametersErrorMessage, out var unquoteParameters, out var allowEmptyParameters)) + { + GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg, allowEmptyParameters, commandLine); + } + else + { + commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg, commandLine); + } + } + } + } + } + + /// + /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses + /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. + /// + /// + /// + private void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches, string commandLine) + { + try + { + string responseFile = FrameworkFileUtilities.FixFilePath(unquotedCommandLineArg.Substring(1)); + + if (responseFile.Length == 0) + { + commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg, commandLine); + } + else if (!FileSystems.Default.FileExists(responseFile)) + { + commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg, commandLine); + } + else + { + // normalize the response file path to help catch multiple (or recursive) inclusions + responseFile = Path.GetFullPath(responseFile); + // NOTE: for network paths or mapped paths, normalization is not guaranteed to work + + bool isRepeatedResponseFile = false; + + foreach (string includedResponseFile in includedResponseFiles) + { + if (string.Equals(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase)) + { + commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg, commandLine); + isRepeatedResponseFile = true; + break; + } + } + + if (!isRepeatedResponseFile) + { + var responseFileDirectory = FrameworkFileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(responseFile)); + includedResponseFiles.Add(responseFile); + + List argsFromResponseFile; + +#if FEATURE_ENCODING_DEFAULT + using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#else + using (StreamReader responseFileContents = FileUtilities.OpenRead(responseFile)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#endif + { + argsFromResponseFile = new List(); + + while (responseFileContents.Peek() != -1) + { + // ignore leading whitespace on each line + string responseFileLine = responseFileContents.ReadLine().TrimStart(); + + // skip comment lines beginning with # + if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) + { + // Allow special case to support a path relative to the .rsp file being processed. + responseFileLine = Regex.Replace(responseFileLine, responseFilePathReplacement, + responseFileDirectory, RegexOptions.IgnoreCase); + + // treat each line of the response file like a command line i.e. args separated by whitespace + argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); + } + } + } + + CommandLineSwitches.SwitchesFromResponseFiles.Add((responseFile, string.Join(" ", argsFromResponseFile))); + + GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches, commandLine); + } + } + } + catch (NotSupportedException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (SecurityException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (UnauthorizedAccessException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (IOException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + } + + /// + /// Called when a switch that doesn't take parameters is detected on the command line. + /// + /// + /// + /// + /// + /// + private static void GatherParameterlessCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + string unquotedCommandLineArg, + string commandLine) + { + // switch should not have any parameters + if (switchParameters.Length == 0) + { + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || + (duplicateSwitchErrorMessage == null)) + { + commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg, commandLine); + } + } + + /// + /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the + /// switch parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static void GatherParameterizedCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + bool multipleParametersAllowed, + string missingParametersErrorMessage, + bool unquoteParameters, + string unquotedCommandLineArg, + bool allowEmptyParameters, + string commandLine) + { + if (// switch must have parameters + (switchParameters.Length > 1) || + // unless the parameters are optional + (missingParametersErrorMessage == null)) + { + // skip the parameter indicator (if any) + if (switchParameters.Length > 0) + { + switchParameters = switchParameters.Substring(1); + } + + if (parameterizedSwitch == CommandLineSwitches.ParameterizedSwitch.Project && IsEnvironmentVariable(switchParameters)) + { + commandLineSwitches.SetSwitchError("EnvironmentVariableAsSwitch", unquotedCommandLineArg, commandLine); + } + + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || + (duplicateSwitchErrorMessage == null)) + { + // save the parameters after unquoting and splitting them if necessary + if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters, allowEmptyParameters)) + { + // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional + if (missingParametersErrorMessage != null) + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + + /// + /// Identifies if there is rsp files near the project file + /// + /// true if there autoresponse file was found + internal bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) + { + bool found = false; + + var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); + + if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + // gather any switches from the first Directory.Build.rsp found in the project directory or above + string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); + + found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); + + // Don't look for more response files if it's only in the same place we already looked (next to the exe) + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + if (!string.Equals(projectDirectory, exePath, StringComparison.OrdinalIgnoreCase)) + { + // this combines any found, with higher precedence, with the switches from the original auto response file switches + found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); + } + } + + return found; + } + + private static string GetProjectDirectory(string[] projectSwitchParameters) + { + string projectDirectory = "."; + ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); + + if (projectSwitchParameters.Length == 1) + { + var projectFile = FrameworkFileUtilities.FixFilePath(projectSwitchParameters[0]); + + if (FileSystems.Default.DirectoryExists(projectFile)) + { + // the provided argument value is actually the directory + projectDirectory = projectFile; + } + else + { + InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); + projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); + } + } + + return projectDirectory; + } + + /// + /// Extracts a switch's parameters after processing all quoting around the switch. + /// + /// + /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". + /// + /// + /// + /// + /// + /// + /// + /// The given switch's parameters (with interesting quoting preserved). + internal static string ExtractSwitchParameters( + string commandLineArg, + string unquotedCommandLineArg, + int doubleQuotesRemovedFromArg, + string switchName, + int switchParameterIndicator, + int switchIndicatorsLength) + { + + // find the parameter indicator again using the quoted arg + // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a + // parameter indicator cannot be escaped or made into a literal + int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); + + // check if there is any quoting in the name portion of the switch + string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); + + ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), + "The switch name extracted from either the partially or completely unquoted arg should be the same."); + + ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, + "The name portion of the switch cannot contain more quoting than the arg itself."); + + string switchParameters; + // if quoting in the name portion of the switch was terminated + if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) + { + // get the parameters exactly as specified on the command line i.e. including quoting + switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); + } + else + { + // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) + // terminates the switch parameters + int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); + if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && + ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) + { + // then the parameters are not quoted in any interesting way, so use the unquoted parameters + switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); + } + else + { + // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name + // portion of the switch + switchParameters = $":\"{commandLineArg.Substring(quotedSwitchParameterIndicator + 1)}"; + } + } + + ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); + + return switchParameters; + } + + /// + /// Checks whether envVar is an environment variable. MSBuild uses + /// Environment.ExpandEnvironmentVariables(string), which only + /// considers %-delimited variables. + /// + /// A possible environment variable + /// Whether envVar is an environment variable + private static bool IsEnvironmentVariable(string envVar) + { + return envVar.StartsWith("%") && envVar.EndsWith("%") && envVar.Length > 1; + } + + /// + /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the + /// switches from the auto-response file with the switches passed in. + /// Returns true if the response file was found. + /// + private bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + string autoResponseFile = Path.Combine(path, autoResponseFileName); + return GatherAutoResponseFileSwitchesFromFullPath(autoResponseFile, switchesFromAutoResponseFile, commandLine); + } + + private bool GatherAutoResponseFileSwitchesFromFullPath(string autoResponseFile, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + bool found = false; + + // if the auto-response file does not exist, only use the switches on the command line + if (FileSystems.Default.FileExists(autoResponseFile)) + { + found = true; + GatherResponseFileSwitch($"@{autoResponseFile}", switchesFromAutoResponseFile, commandLine); + + // if the "/noautoresponse" switch was set in the auto-response file, flag an error + if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", + switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse), commandLine); + } + + // Throw errors found in the response file + switchesFromAutoResponseFile.ThrowErrors(); + } + + return found; + } + + /// + /// Checks whether an argument given as a parameter starts with valid indicator, + ///
which means, whether switch begins with one of: "/", "-", "--" + ///
+ /// Command line argument with beginning indicator (e.g. --help). + ///
This argument has to be unquoted, otherwise the first character will always be a quote character " + /// true if argument's beginning matches one of possible indicators + ///
false if argument's beginning doesn't match any of correct indicator + ///
+ private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) + { + return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" + || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); + } + + /// + /// Gets the length of the switch indicator (- or / or --) + ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. + ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. + ///
+ /// Unquoted argument with leading indicator and name + /// Correct length of used indicator + ///
0 if no leading sequence recognized as correct indicator
+ /// Internal for testing purposes + internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) + { + if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) + { + return 2; + } + else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) + { + return 1; + } + else + { + return 0; + } + } + + public void ResetGatheringSwitchesState() + { + includedResponseFiles = new List(); + CommandLineSwitches.SwitchesFromResponseFiles = new(); + } + } +} diff --git a/src/MSBuild/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs similarity index 99% rename from src/MSBuild/CommandLineSwitchException.cs rename to src/MSBuild/CommandLine/CommandLineSwitchException.cs index e8ce5dd036d..54ec5e65ef2 100644 --- a/src/MSBuild/CommandLineSwitchException.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchException.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This exception is used to flag (syntax) errors in command line switches passed to the application. diff --git a/src/MSBuild/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs similarity index 98% rename from src/MSBuild/CommandLineSwitches.cs rename to src/MSBuild/CommandLine/CommandLineSwitches.cs index 66b529eb62f..7c7add45c3e 100644 --- a/src/MSBuild/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter @@ -120,6 +120,7 @@ internal enum ParameterizedSwitch GetResultOutputFile, FeatureAvailability, MultiThreaded, + ParentPacketVersion, // This has to be kept as last enum value NumberOfParameterizedSwitches, } @@ -277,7 +278,7 @@ internal ParameterizedSwitchInfo( new ParameterizedSwitchInfo( ["warnnotaserror", "noerr"], ParameterizedSwitch.WarningsNotAsErrors, null, true, "MissingWarnNotAsErrorParameterError", true, false, "HelpMessage_40_WarnNotAsErrorSwitch"), new ParameterizedSwitchInfo( ["warnasmessage", "nowarn"], ParameterizedSwitch.WarningsAsMessages, null, true, "MissingWarnAsMessageParameterError", true, false, "HelpMessage_29_WarnAsMessageSwitch"), new ParameterizedSwitchInfo( ["binarylogger", "bl"], ParameterizedSwitch.BinaryLogger, null, false, null, true, false, "HelpMessage_30_BinaryLoggerSwitch"), - new ParameterizedSwitchInfo( ["check"], ParameterizedSwitch.Check, null, false, null, true, false, "HelpMessage_52_BuildCheckSwitch"), + new ParameterizedSwitchInfo( ["check"], ParameterizedSwitch.Check, null, false, null, true, false, "HelpMessage_52_BuildCheckSwitch"), new ParameterizedSwitchInfo( ["restore", "r"], ParameterizedSwitch.Restore, null, false, null, true, false, "HelpMessage_31_RestoreSwitch"), new ParameterizedSwitchInfo( ["profileevaluation", "prof"], ParameterizedSwitch.ProfileEvaluation, null, false, "MissingProfileParameterError", true, false, "HelpMessage_32_ProfilerSwitch"), new ParameterizedSwitchInfo( ["restoreproperty", "rp"], ParameterizedSwitch.RestoreProperty, null, true, "MissingPropertyError", true, false, "HelpMessage_33_RestorePropertySwitch"), @@ -297,7 +298,9 @@ internal ParameterizedSwitchInfo( new ParameterizedSwitchInfo( ["getTargetResult"], ParameterizedSwitch.GetTargetResult, null, true, "MissingGetTargetResultError", true, false, "HelpMessage_45_GetTargetResultSwitch"), new ParameterizedSwitchInfo( ["getResultOutputFile"], ParameterizedSwitch.GetResultOutputFile, null, true, "MissingGetResultFileError", true, false, "HelpMessage_51_GetResultOutputFileSwitch"), new ParameterizedSwitchInfo( ["featureAvailability", "fa"], ParameterizedSwitch.FeatureAvailability, null, true, "MissingFeatureAvailabilityError", true, false, "HelpMessage_46_FeatureAvailabilitySwitch"), - new ParameterizedSwitchInfo( ["multithreaded", "mt"], ParameterizedSwitch.MultiThreaded, null, false, null, true, false, "HelpMessage_49_MultiThreadedSwitch") + new ParameterizedSwitchInfo( ["multithreaded", "mt"], ParameterizedSwitch.MultiThreaded, null, false, null, true, false, "HelpMessage_49_MultiThreadedSwitch"), + new ParameterizedSwitchInfo( ["parentpacketversion"], ParameterizedSwitch.ParentPacketVersion, null, false, null, false, false, null), + // Add to ParameterizedSwitch enum (before NumberOfParameterizedSwitches): }; /// @@ -423,6 +426,7 @@ private struct DetectedParameterlessSwitch /// /// This struct stores the details of a switch that takes parameters that is detected on the command line. /// + [DebuggerDisplay("{commandLineArg} | {parameters}")] private struct DetectedParameterizedSwitch { // the actual text of the switch @@ -432,8 +436,8 @@ private struct DetectedParameterizedSwitch internal ArrayList parameters; } - // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the - // command line + // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the command + // line private DetectedParameterlessSwitch[] _parameterlessSwitches; // for each recognized switch that takes parameters, this array indicates if the switch has been detected on the command // line, and it provides a store for the switch parameters @@ -467,8 +471,11 @@ internal CommandLineSwitches() { Debug.Assert(i == (int)(s_parameterizedSwitchesMap[i].parameterizedSwitch), "The map of parameterized switches must be ordered the same way as the ParameterizedSwitch enumeration."); - if (s_parameterizedSwitchesMap[i].parameterizedSwitch is not ParameterizedSwitch.Project and - not ParameterizedSwitch.NodeMode and not ParameterizedSwitch.Check) + if (s_parameterizedSwitchesMap[i].parameterizedSwitch + is not ParameterizedSwitch.Project + and not ParameterizedSwitch.NodeMode + and not ParameterizedSwitch.Check + and not ParameterizedSwitch.ParentPacketVersion) { Debug.Assert(!string.IsNullOrEmpty(s_parameterizedSwitchesMap[i].resourceId), "All parameterized switches should be cross-checked against the help message strings except from project switch"); } @@ -498,6 +505,7 @@ internal void SetParameterlessSwitch(ParameterlessSwitch parameterlessSwitch, st /// /// /// + /// /// true, if the given parameters were successfully stored internal bool SetParameterizedSwitch( ParameterizedSwitch parameterizedSwitch, diff --git a/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs new file mode 100644 index 00000000000..66bf8c8582e --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.Build.CommandLine.Experimental.CommandLineSwitches; + +namespace Microsoft.Build.CommandLine.Experimental +{ + internal readonly struct CommandLineSwitchesAccessor + { + private readonly CommandLineSwitches switches; + + internal CommandLineSwitchesAccessor(CommandLineSwitches switches) + { + this.switches = switches; + } + + // Parameterless switches + public bool? Help => GetParameterlessSwitchValue(ParameterlessSwitch.Help); + + public bool? Version => GetParameterlessSwitchValue(ParameterlessSwitch.Version); + + public bool? NoLogo => GetParameterlessSwitchValue(ParameterlessSwitch.NoLogo); + + public bool? NoAutoResponse => GetParameterlessSwitchValue(ParameterlessSwitch.NoAutoResponse); + + public bool? NoConsoleLogger => GetParameterlessSwitchValue(ParameterlessSwitch.NoConsoleLogger); + + public bool? FileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger); + + public bool? FileLogger1 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger1); + + public bool? FileLogger2 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger2); + + public bool? FileLogger3 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger3); + + public bool? FileLogger4 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger4); + + public bool? FileLogger5 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger5); + + public bool? FileLogger6 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger6); + + public bool? FileLogger7 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger7); + + public bool? FileLogger8 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger8); + + public bool? FileLogger9 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger9); + + public bool? DistributedFileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.DistributedFileLogger); + +#if DEBUG + public bool? WaitForDebugger => GetParameterlessSwitchValue(ParameterlessSwitch.WaitForDebugger); +#endif + + // Parameterized switches + public string[]? Project => GetParameterizedSwitchValue(ParameterizedSwitch.Project); + + public string[]? Target => GetParameterizedSwitchValue(ParameterizedSwitch.Target); + + public string[]? Property => GetParameterizedSwitchValue(ParameterizedSwitch.Property); + + public string[]? Logger => GetParameterizedSwitchValue(ParameterizedSwitch.Logger); + + public string[]? DistributedLogger => GetParameterizedSwitchValue(ParameterizedSwitch.DistributedLogger); + + public string[]? Verbosity => GetParameterizedSwitchValue(ParameterizedSwitch.Verbosity); + +#if FEATURE_XML_SCHEMA_VALIDATION + public string[]? Validate => GetParameterizedSwitchValue(ParameterizedSwitch.Validate); +#endif + + public string[]? ConsoleLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.ConsoleLoggerParameters); + + public string[]? NodeMode => GetParameterizedSwitchValue(ParameterizedSwitch.NodeMode); + + public string[]? MaxCpuCount => GetParameterizedSwitchValue(ParameterizedSwitch.MaxCPUCount); + + public string[]? IgnoreProjectExtensions => GetParameterizedSwitchValue(ParameterizedSwitch.IgnoreProjectExtensions); + + public string[]? ToolsVersion => GetParameterizedSwitchValue(ParameterizedSwitch.ToolsVersion); + + public string[]? FileLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters); + + public string[]? FileLoggerParameters1 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters1); + + public string[]? FileLoggerParameters2 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters2); + + public string[]? FileLoggerParameters3 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters3); + + public string[]? FileLoggerParameters4 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters4); + + public string[]? FileLoggerParameters5 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters5); + + public string[]? FileLoggerParameters6 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters6); + + public string[]? FileLoggerParameters7 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters7); + + public string[]? FileLoggerParameters8 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters8); + + public string[]? FileLoggerParameters9 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters9); + + public string[]? TerminalLogger => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLogger); + + public string[]? TerminalLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLoggerParameters); + + public string[]? NodeReuse => GetParameterizedSwitchValue(ParameterizedSwitch.NodeReuse); + + public string[]? Preprocess => GetParameterizedSwitchValue(ParameterizedSwitch.Preprocess); + + public string[]? Targets => GetParameterizedSwitchValue(ParameterizedSwitch.Targets); + + public string[]? WarningsAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsErrors); + + public string[]? WarningsNotAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsNotAsErrors); + + public string[]? WarningsAsMessages => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsMessages); + + public string[]? BinaryLogger => GetParameterizedSwitchValue(ParameterizedSwitch.BinaryLogger); + + public string[]? Check => GetParameterizedSwitchValue(ParameterizedSwitch.Check); + + public string[]? Restore => GetParameterizedSwitchValue(ParameterizedSwitch.Restore); + + public string[]? ProfileEvaluation => GetParameterizedSwitchValue(ParameterizedSwitch.ProfileEvaluation); + + public string[]? RestoreProperty => GetParameterizedSwitchValue(ParameterizedSwitch.RestoreProperty); + + public string[]? Interactive => GetParameterizedSwitchValue(ParameterizedSwitch.Interactive); + + public string[]? IsolateProjects => GetParameterizedSwitchValue(ParameterizedSwitch.IsolateProjects); + + public string[]? GraphBuild => GetParameterizedSwitchValue(ParameterizedSwitch.GraphBuild); + + public string[]? InputResultsCaches => GetParameterizedSwitchValue(ParameterizedSwitch.InputResultsCaches); + + public string[]? OutputResultsCache => GetParameterizedSwitchValue(ParameterizedSwitch.OutputResultsCache); + +#if FEATURE_REPORTFILEACCESSES + public string[]? ReportFileAccesses => GetParameterizedSwitchValue(ParameterizedSwitch.ReportFileAccesses); +#endif + + public string[]? LowPriority => GetParameterizedSwitchValue(ParameterizedSwitch.LowPriority); + + public string[]? Question => GetParameterizedSwitchValue(ParameterizedSwitch.Question); + + public string[]? DetailedSummary => GetParameterizedSwitchValue(ParameterizedSwitch.DetailedSummary); + + public string[]? GetProperty => GetParameterizedSwitchValue(ParameterizedSwitch.GetProperty); + + public string[]? GetItem => GetParameterizedSwitchValue(ParameterizedSwitch.GetItem); + + public string[]? GetTargetResult => GetParameterizedSwitchValue(ParameterizedSwitch.GetTargetResult); + + public string[]? GetResultOutputFile => GetParameterizedSwitchValue(ParameterizedSwitch.GetResultOutputFile); + + public string[]? FeatureAvailability => GetParameterizedSwitchValue(ParameterizedSwitch.FeatureAvailability); + + public string[]? MultiThreaded => GetParameterizedSwitchValue(ParameterizedSwitch.MultiThreaded); + + private bool? GetParameterlessSwitchValue(ParameterlessSwitch switchType) => switches.IsParameterlessSwitchSet(switchType) ? switches[switchType] : null; + + private string[]? GetParameterizedSwitchValue(ParameterizedSwitch switchType) => switches.IsParameterizedSwitchSet(switchType) ? switches[switchType] : null; + } +} diff --git a/src/MSBuild/JsonOutputFormatter.cs b/src/MSBuild/JsonOutputFormatter.cs index cdc166849ca..a3c2f0afc9d 100644 --- a/src/MSBuild/JsonOutputFormatter.cs +++ b/src/MSBuild/JsonOutputFormatter.cs @@ -67,7 +67,7 @@ internal void AddItemInstancesInJsonFormat(string[] itemNames, ProjectInstance p continue; } - jsonItem[metadatumName] = item.GetMetadataValue(metadatumName); + jsonItem[metadatumName] = TryGetMetadataValue(item, metadatumName); } itemArray.Add(jsonItem); @@ -108,7 +108,7 @@ internal void AddItemsInJsonFormat(string[] itemNames, Project project) continue; } - jsonItem[metadatumName] = item.GetMetadataValue(metadatumName); + jsonItem[metadatumName] = TryGetMetadataValue(item, metadatumName); } itemArray.Add(jsonItem); @@ -147,7 +147,7 @@ internal void AddTargetResultsInJsonFormat(string[] targetNames, BuildResult res continue; } - jsonItem[metadatumName] = item.GetMetadata(metadatumName); + jsonItem[metadatumName] = TryGetMetadata(item, metadatumName); } outputArray.Add(jsonItem); @@ -159,5 +159,62 @@ internal void AddTargetResultsInJsonFormat(string[] targetNames, BuildResult res _topLevelNode["TargetResults"] = targetResultsNode; } + + /// + /// Attempts to get metadata from an ITaskItem. If the metadata is a built-in metadata + /// (like FullPath, Directory, etc.) and the item spec contains illegal path characters, + /// this will catch the InvalidOperationException and return an empty string. + /// + private static string TryGetMetadata(ITaskItem item, string metadataName) + { + try + { + return item.GetMetadata(metadataName); + } + catch (InvalidOperationException) + { + // Built-in metadata like FullPath, Directory, etc. require path computation. + // If the item spec contains illegal path characters, return empty string. + return string.Empty; + } + } + + /// + /// Attempts to get metadata value from a ProjectItemInstance. If the metadata is a built-in metadata + /// (like FullPath, Directory, etc.) and the item spec contains illegal path characters, + /// this will catch the InvalidOperationException and return an empty string. + /// + private static string TryGetMetadataValue(ProjectItemInstance item, string metadataName) + { + try + { + return item.GetMetadataValue(metadataName); + } + catch (InvalidOperationException) + { + // Built-in metadata like FullPath, Directory, etc. require path computation. + // If the item spec contains illegal path characters, return empty string. + return string.Empty; + } + } + + /// + /// Attempts to get metadata value from a ProjectItem. If the metadata is a built-in metadata + /// (like FullPath, Directory, etc.) and the item spec contains illegal path characters, + /// this will catch the InvalidOperationException and return an empty string. + /// + private static string TryGetMetadataValue(ProjectItem item, string metadataName) + { + try + { + return item.GetMetadataValue(metadataName); + } + catch (InvalidOperationException) + { + // Built-in metadata like FullPath, Directory, etc. require path computation. + // If the item spec contains illegal path characters, return empty string. + return string.Empty; + } + } } } diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index e5883bd64f6..781af0730a3 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -134,9 +134,11 @@ - - + + + + @@ -195,6 +197,7 @@ + @@ -210,7 +213,7 @@ - + @@ -316,3 +319,4 @@ + \ No newline at end of file diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index 3eeb975bc40..33100583fe2 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -34,18 +34,12 @@ internal static class MSBuildClientApp /// /// The locations of msbuild exe/dll and dotnet.exe would be automatically detected if called from dotnet or msbuild cli. Calling this function from other executables might not work. /// - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, CancellationToken cancellationToken) { string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; return Execute( - commandLine, + commandLineArgs, msbuildLocation, cancellationToken); } @@ -53,7 +47,7 @@ public static MSBuildApp.ExitType Execute( /// /// This is the entry point for the MSBuild client. /// - /// The command line to process. The first argument + /// The command line to process. The first argument /// on the command line is assumed to be the name/path of the executable, and /// is ignored. /// Full path to current MSBuild.exe if executable is MSBuild.exe, @@ -61,16 +55,9 @@ public static MSBuildApp.ExitType Execute( /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation, - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbuildLocation, CancellationToken cancellationToken) { - MSBuildClient msbuildClient = new MSBuildClient(commandLine, msbuildLocation); + MSBuildClient msbuildClient = new MSBuildClient(commandLineArgs, msbuildLocation); MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || @@ -84,7 +71,7 @@ public static MSBuildApp.ExitType Execute( } // Server is busy, fallback to old behavior. - return MSBuildApp.Execute(commandLine); + return MSBuildApp.Execute(commandLineArgs); } if (exitResult.MSBuildClientExitType == MSBuildClientExitType.Success && diff --git a/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs b/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs index cc08771ceb9..9769d7b6b59 100644 --- a/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs +++ b/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs @@ -15,16 +15,18 @@ namespace Microsoft.Build.CommandLine internal class NodeEndpointOutOfProcTaskHost : NodeEndpointOutOfProcBase { internal bool _nodeReuse; - + #region Constructors and Factories /// - /// Instantiates an endpoint to act as a client + /// Instantiates an endpoint to act as a client. /// - internal NodeEndpointOutOfProcTaskHost(bool nodeReuse) + /// Whether node reuse is enabled. + /// The packet version supported by the parent. 1 if parent doesn't support version negotiation. + internal NodeEndpointOutOfProcTaskHost(bool nodeReuse, byte parentPacketVersion) { _nodeReuse = nodeReuse; - InternalConstruct(); + InternalConstruct(pipeName: null, parentPacketVersion); } #endregion // Constructors and Factories diff --git a/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs b/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs index b35b83b8488..ae77fb43e03 100644 --- a/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs +++ b/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs @@ -11,6 +11,9 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +#if !NET35 +using Microsoft.Build.Execution; +#endif #nullable disable @@ -30,6 +33,7 @@ internal class OutOfProcTaskAppDomainWrapperBase /// private ITask wrappedTask; + #if FEATURE_APPDOMAIN /// /// This is an appDomain instance if any is created for running this task @@ -54,6 +58,10 @@ internal class OutOfProcTaskAppDomainWrapperBase /// private string taskName; +#if !NET35 + private HostServices _hostServices; +#endif + /// /// This is the actual user task whose instance we will create and invoke Execute /// @@ -86,6 +94,7 @@ internal bool CancelPending /// The path to the project file in which the task invocation is located. /// The line in the project file where the task invocation is located. /// The column in the project file where the task invocation is located. + /// The target name that invokes this task. /// The AppDomainSetup that we want to use to launch our AppDomainIsolated tasks /// Parameters that will be passed to the task when created /// Task completion result showing success, failure or if there was a crash @@ -96,8 +105,13 @@ internal OutOfProcTaskHostTaskResult ExecuteTask( string taskFile, int taskLine, int taskColumn, + string targetName, + string projectFile, #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, +#endif +#if !NET35 + HostServices hostServices, #endif IDictionary taskParams) { @@ -106,6 +120,9 @@ internal OutOfProcTaskHostTaskResult ExecuteTask( #if FEATURE_APPDOMAIN _taskAppDomain = null; +#endif +#if !NET35 + _hostServices = hostServices; #endif wrappedTask = null; @@ -137,7 +154,16 @@ internal OutOfProcTaskHostTaskResult ExecuteTask( if (taskType.HasSTAThreadAttribute) { #if FEATURE_APARTMENT_STATE - taskResult = InstantiateAndExecuteTaskInSTAThread(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn, + taskResult = InstantiateAndExecuteTaskInSTAThread( + oopTaskHostNode, + taskType, + taskName, + taskLocation, + taskFile, + taskLine, + taskColumn, + targetName, + projectFile, #if FEATURE_APPDOMAIN appDomainSetup, #endif @@ -152,7 +178,16 @@ internal OutOfProcTaskHostTaskResult ExecuteTask( } else { - taskResult = InstantiateAndExecuteTask(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn, + taskResult = InstantiateAndExecuteTask( + oopTaskHostNode, + taskType, + taskName, + taskLocation, + taskFile, + taskLine, + taskColumn, + targetName, + projectFile, #if FEATURE_APPDOMAIN appDomainSetup, #endif @@ -196,6 +231,8 @@ private OutOfProcTaskHostTaskResult InstantiateAndExecuteTaskInSTAThread( string taskFile, int taskLine, int taskColumn, + string targetName, + string projectFile, #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, #endif @@ -219,6 +256,8 @@ private OutOfProcTaskHostTaskResult InstantiateAndExecuteTaskInSTAThread( taskFile, taskLine, taskColumn, + targetName, + projectFile, #if FEATURE_APPDOMAIN appDomainSetup, #endif @@ -275,6 +314,8 @@ private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask( string taskFile, int taskLine, int taskColumn, + string targetName, + string projectFile, #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, #endif @@ -306,6 +347,14 @@ private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask( #endif ); #pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + +#if !NET35 + if (projectFile != null && _hostServices != null) + { + wrappedTask.HostObject = _hostServices.GetHostObject(projectFile, targetName, taskName); + } +#endif + wrappedTask.BuildEngine = oopTaskHostNode; } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index d141991394f..aa7afd2efa4 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -644,7 +644,7 @@ public void PacketReceived(int node, INodePacket packet) /// /// The exception which caused shutdown, if any. /// The reason for shutting down. - public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeReuse = false) + public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeReuse = false, byte parentPacketVersion = 1) { #if !CLR2COMPATIBILITY _registeredTaskObjectCache = new RegisteredTaskObjectCacheBase(); @@ -655,7 +655,7 @@ public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeRe _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); _nodeReuse = nodeReuse; - _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(nodeReuse); + _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(nodeReuse, parentPacketVersion); _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged); _nodeEndpoint.Listen(this); @@ -958,8 +958,13 @@ private void RunTask(object state) taskConfiguration.ProjectFileOfTask, taskConfiguration.LineNumberOfTask, taskConfiguration.ColumnNumberOfTask, + taskConfiguration.TargetName, + taskConfiguration.ProjectFile, #if FEATURE_APPDOMAIN taskConfiguration.AppDomainSetup, +#endif +#if !NET35 + taskConfiguration.HostServices, #endif taskParams); } diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 701bb681d2c..6e75cc0db7e 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1700,10 +1700,29 @@ 0: turned off + + Duplicate binary log path(s) specified and ignored: {0} + {0} is the list of duplicate paths that were filtered out + + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index c050d60e03e..c71a71dfa0c 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: Přepínač -noAutoResponse nelze zadat v souboru automatických odpovědí MSBuild.rsp ani v žádném jiném souboru odpovědí, na který se v souboru automatických odpovědí odkazuje. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Byly zadány duplicitní cesty k binárnímu protokolu a byly ignorovány: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Při zpracování proměnné prostředí MSBUILD_LOGGING_ARGS došlo k chybě: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Nepodporovaný argument {0} se ignoruje. Jsou povoleny pouze argumenty -bl a -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Používají se argumenty z proměnné prostředí MSBUILD_LOGGING_ARGS: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Na základě klíče registru Windows LongPathsEnabled má funkce LongPaths hodnotu {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 30794b9ef29..3fd592924a9 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: Der Schalter "-noAutoResponse" kann weder in der automatischen Antwortdatei "MSBuild.rsp" noch in einer anderen Antwortdatei verwendet werden, auf die die automatische Antwortdatei verweist. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Doppelte Binärprotokollpfade angegeben und ignoriert: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Fehler beim Verarbeiten der Umgebungsvariable MSBUILD_LOGGING_ARGS: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Das nicht unterstützte Argument „{0}“ wird ignoriert. Zulässig sind nur die Argumente -bl und -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Es werden Argumente aus der Umgebungsvariable MSBUILD_LOGGING_ARGS verwendet: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Basierend auf dem Windows-Registrierungsschlüssel LongPathsEnabled ist das Feature LongPaths {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index a96c9ca8c11..1918a695744 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: El modificador -noAutoResponse no puede especificarse en el archivo de respuesta automática MSBuild.rsp ni en ningún archivo de respuesta al que el archivo de respuesta automática haga referencia. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Rutas de registro binarias duplicadas especificadas y omitidas: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -329,6 +334,21 @@ Esta marca es experimental y puede que no funcione según lo previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: error al procesar MSBUILD_LOGGING_ARGS variable de entorno: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: se omite el argumento no admitido ''{0}''. Solo se permiten los argumentos -bl y -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Uso de argumentos de MSBUILD_LOGGING_ARGS variable de entorno: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. De acuerdo con la clave del Registro de Windows LongPathsEnabled, la característica LongPaths está {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index cca6ab9830e..e8a2694729e 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: Impossible de spécifier le commutateur -noAutoResponse dans le fichier réponse automatique MSBuild.rsp, ni dans aucun autre fichier réponse référencé par le fichier réponse automatique. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Chemins d'accès aux journaux binaires en double spécifiés et ignorés : {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ futures LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: erreur lors du traitement de la variable d’environnement MSBUILD_LOGGING_ARGS : {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS : argument non pris en charge ignoré « {0} ». Seuls les arguments -bl et -check sont autorisés. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Utilisation des arguments de la variable d’environnement MSBUILD_LOGGING_ARGS : {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. D’après la clé de Registre Windows LongPathsEnabled, la fonctionnalité LongPaths est {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 26f3d724edb..8518442aadf 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: non è possibile specificare l'opzione -noAutoResponse nel file di risposta automatica MSBuild.rsp o in file di risposta a cui il file di risposta automatica fa riferimento. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Percorsi di log binari duplicati specificati e ignorati: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: errore di elaborazione della variabile di ambiente MSBUILD_LOGGING_ARGS: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: l'argomento '{0}' non supportato viene ignorato. Sono consentiti solo gli argomenti -bl e -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Utilizzo di argomenti dalla variabile di ambiente MSBUILD_LOGGING_ARGS: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. In base alla chiave del Registro di sistema di Windows LongPathsEnabled, la funzionalità LongPaths è {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 5d1f7dacd88..162d0b90a38 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: MSBuild.rsp 自動応答ファイルや、自動応答ファイルによって参照される応答ファイルに -noAutoResponse スイッチを指定することはできません。 {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + 重複するバイナリ ログ パスが指定され、次は無視されました: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: MSBUILD_LOGGING_ARGS 環境変数の処理中にエラーが発生しました: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: サポートされていない引数 '{0}' を無視します。使用できる引数は -bl と -check のみです。 + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD_LOGGING_ARGS 環境変数からの引数を使用しています: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows レジストリ キー LongPathsEnabled に基づいて、LongPaths 機能は{0}です。 diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index b5691e61719..cf2d8bd921d 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: MSBuild.rsp 자동 지시 파일과 자동 지시 파일에서 참조하는 모든 지시 파일에는 -noAutoResponse 스위치를 지정할 수 없습니다. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + 중복된 이진 로그 경로가 지정되어 무시되었습니다. {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -331,6 +336,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: MSBUILD_LOGGING_ARGS 환경 변수를 처리하는 중 오류가 발생했습니다. {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: 지원되지 않는 인수 '{0}'을(를) 무시합니다. -bl과 -check 인수만 허용됩니다. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD_LOGGING_ARGS 환경 변수에서 다음 인수를 사용합니다. {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows 레지스트리 키 LongPathsEnabled에 따라 LongPaths 기능이 {0}입니다. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 03b0e01f39c..c7858eb0224 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: przełącznika -noAutoResponse nie można określić w pliku autoodpowiedzi MSBuild.rsp ani w żadnym pliku odpowiedzi, do którego odwołuje się plik autoodpowiedzi. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Określono i zignorowano zduplikowane ścieżki dziennika binarnego: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -329,6 +334,21 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: błąd podczas przetwarzania zmiennej środowiskowej MSBUILD_LOGGING_ARGS: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: ignorowanie nieobsługiwanego argumentu „{0}”. Dozwolone są tylko argumenty -bl i -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Używanie argumentów ze zmiennej środowiskowej MSBUILD_LOGGING_ARGS: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Na podstawie klucza rejestru systemu Windows LongPathsEnabled funkcja LongPaths jest {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 76cbbebd996..362ffcee4f7 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: A opção /noAutoResponse não pode ser especificada no arquivo de resposta automática MSBuild.rsp nem em qualquer arquivo de resposta usado como referência para o arquivo de resposta automática. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Caminhos de log binários duplicados especificados e ignorados: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -329,6 +334,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: erro ao processar MSBUILD_LOGGING_ARGS de ambiente: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: ignorando argumento sem suporte "{0}". Somente os argumentos -bl e -check são permitidos. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Usando argumentos de uma MSBUILD_LOGGING_ARGS de ambiente: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Com base na chave do Registro do Windows LongPathsEnabled, o recurso LongPaths é {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index cec4de53558..68442ee1ff3 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: ключ noAutoResponse не может быть указан в файле автоответа MSBuild.rsp или в любом другом файле ответа, на который файл автоответа ссылается. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Указаны повторяющиеся пути к двоичным журналам, которые были проигнорированы: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -329,6 +334,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: ошибка при обработке переменной среды MSBUILD_LOGGING_ARGS: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: игнорируется неподдерживаемый аргумент "{0}". Допустимы только аргументы -bl и -check. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Используются аргументы из переменной среды MSBUILD_LOGGING_ARGS: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. На основе раздела реестра Windows LongPathsEnabled функция LongPaths имеет значение {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 940f30c6aa8..cee2f8b008f 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: -noAutoResponse anahtarı, MSBuild.rsp otomatik yanıt dosyasında ve bu dosyanın başvuruda bulunduğu herhangi bir yanıt dosyasında belirtilemez. {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + Belirtilen ve yok sayılan yinelenen ikili günlük yolları: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -329,6 +334,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: MSBUILD_LOGGING_ARGS ortam değişkeni işlenirken hata oluştu: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Desteklenmeyen '{0}' bağımsız değişkeni yoksayılıyor. Yalnızca -bl ve -check bağımsız değişkenlerine izin verilir. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD_LOGGING_ARGS ortam değişkeninden alınan bağımsız değişkenler kullanılıyor: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows kayıt defteri anahtarı LongPathsEnabled ayarına bağlı olarak LongPaths özelliği {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index f9cd58c592f..985a1b300fb 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: 不能在 MSBuild.rsp 自动响应文件中或由该自动响应文件引用的任何响应文件中指定 -noAutoResponse 开关。 {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + 指定且忽略的重复二进制日志路径: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: 处理 MSBUILD_LOGGING_ARGS 环境变量时出错: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: 忽略不受支持的参数“{0}”。仅允许 -bl 和 -check 参数。 + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + 正在使用 MSBUILD_LOGGING_ARGS 环境变量中的参数: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. 基于 Windows 注册表项 LongPathsEnabled,LongPaths 功能为 {0}。 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 3bb145115b7..8fe44e36d1e 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -33,6 +33,11 @@ MSBUILD : error MSB1027: -noAutoResponse 參數不能在 MSBuild.rsp 自動回應檔中指定,也不能在自動回應檔所參考的任何回應檔中指定。 {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "-noAutoResponse" and "MSBuild.rsp" should not be localized. + + Duplicate binary log path(s) specified and ignored: {0} + 指定了重複的二進位記錄路徑,並已忽略: {0} + {0} is the list of duplicate paths that were filtered out + -question (Experimental) Question whether there is any build work. @@ -330,6 +335,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: 處理 MSBUILD_LOGGING_ARGS 環境變數時發生錯誤: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: 忽略不支援的引數 '{0}'。僅允許使用 -bl 和 -check 引數。 + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + 正在使用 MSBUILD_LOGGING_ARGS 環境變數中的引數: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. 根據 Windows 登錄機碼 LongPathsEnabled,LongPaths 功能為 {0}。 diff --git a/src/MSBuild/ValidateMSBuildPackageDependencyVersions.cs b/src/MSBuild/ValidateMSBuildPackageDependencyVersions.cs index f68a6317b66..af57996ef8d 100644 --- a/src/MSBuild/ValidateMSBuildPackageDependencyVersions.cs +++ b/src/MSBuild/ValidateMSBuildPackageDependencyVersions.cs @@ -20,11 +20,11 @@ public class ValidateMSBuildPackageDependencyVersions : Task // Microsoft.Build.Conversion.Core and Microsoft.Build.Engine are deprecated, but they're still used in VS for now. This project doesn't directly reference them, so they don't appear in its output directory. // Microsoft.NET.StringTools uses API not available in net35, but since we need it to work for TaskHosts as well, there are simpler versions implemented for that. Ensure it's the right version. // Microsoft.Activities.Build and XamlBuildTask are loaded within an AppDomain in the XamlBuildTask after having been loaded from the GAC elsewhere. See https://github.com/dotnet/msbuild/pull/856 - private string[] assembliesToIgnore = { "Microsoft.Build.Conversion.Core", "Microsoft.NET.StringTools.net35", "Microsoft.Build.Engine", "Microsoft.Activities.Build", "XamlBuildTask" }; + private string[] assembliesToIgnore = { "Microsoft.Build.Conversion.Core", "Microsoft.NET.StringTools.net35", "Microsoft.Build.Engine", "Microsoft.Activities.Build", "System.ValueTuple", "XamlBuildTask" }; public override bool Execute() { - bool foundSystemValueTuple = false; + // bool foundSystemValueTuple = false; var settings = new XmlReaderSettings { @@ -94,7 +94,7 @@ public override bool Execute() if (String.Equals(name, "System.ValueTuple", StringComparison.OrdinalIgnoreCase) && String.Equals(version, "4.0.0.0") && String.Equals(assemblyVersion, "4.0.3.0")) { - foundSystemValueTuple = true; + // foundSystemValueTuple = true; } else { @@ -113,11 +113,11 @@ public override bool Execute() } } } - - if (!foundSystemValueTuple) - { - Log.LogError("Binding redirect for 'System.ValueTuple' missing."); - } + + // if (!foundSystemValueTuple) + // { + // Log.LogError("Binding redirect for 'System.ValueTuple' missing."); + // } return !Log.HasLoggedErrors; } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 0bc9bd7aafd..113f7f0602c 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -36,6 +36,7 @@ using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; +using Microsoft.Build.CommandLine.Experimental; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; using FileLogger = Microsoft.Build.Logging.FileLogger; @@ -143,6 +144,8 @@ public enum ExitType private static readonly char[] s_commaSemicolon = { ',', ';' }; + private static CommandLineParser commandLineParser; + /// /// Static constructor /// @@ -158,6 +161,7 @@ static MSBuildApp() // any configuration file exceptions can be caught here. // //////////////////////////////////////////////////////////////////////////////// s_exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); + commandLineParser = new CommandLineParser(); s_initialized = true; } @@ -233,14 +237,18 @@ private static void HandleConfigurationException(Exception ex) #if FEATURE_APPDOMAIN [LoaderOptimization(LoaderOptimization.MultiDomain)] #endif -#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter - public static int Main( -#if !FEATURE_GET_COMMANDLINE - string[] args -#endif - ) -#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + public static int Main(string[] args) { + // When running on CoreCLR(.NET), insert the command executable path as the first element of the args array. + // This is needed because on .NET the first element of Environment.CommandLine is the dotnet executable path + // and not the msbuild executable path. CoreCLR version didn't support Environment.CommandLine initially, so + // workaround was needed. +#if NET + args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args]; +#else + args = QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); +#endif + // Setup the console UI. using AutomaticEncodingRestorer _ = new(); SetConsoleUI(); @@ -248,9 +256,9 @@ string[] args DebuggerLaunchCheck(); // Initialize new build telemetry and record start of this build. - KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow }; - // Initialize OpenTelemetry infrastructure - OpenTelemetryManager.Instance.Initialize(isStandalone: true); + KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; + + TelemetryManager.Instance?.Initialize(isStandalone: true); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -263,66 +271,51 @@ string[] args if ( Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine)) -#else - ConstructArrayArg(args))) -#endif + CanRunServerBasedOnCommandLineSwitches(args)) { Console.CancelKeyPress += Console_CancelKeyPress; // Use the client app to execute build in msbuild server. Opt-in feature. - exitCode = ((s_initialized && MSBuildClientApp.Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine, -#else - ConstructArrayArg(args), -#endif - s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && MSBuildClientApp.Execute(args, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { // return 0 on success, non-zero on failure - exitCode = ((s_initialized && Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine) -#else - ConstructArrayArg(args)) -#endif - == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && Execute(args) == ExitType.Success) ? 0 : 1); } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") { DumpCounters(false /* log to console */); } - OpenTelemetryManager.Instance.Shutdown(); + + TelemetryManager.Instance?.Dispose(); return exitCode; } - /// /// Returns true if arguments allows or make sense to leverage msbuild server. /// /// /// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway. /// - private static bool CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) { bool canRunServer = true; try { - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine); + commandLineParser.GatherAllSwitches( + commandLine, + s_globalMessagesToLogInBuildLoggers, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine, + out s_exeName); + CommandLineSwitches commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); - if (CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) + if (commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) { commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } @@ -353,23 +346,6 @@ private static bool CanRunServerBasedOnCommandLineSwitches( return canRunServer; } -#if !FEATURE_GET_COMMANDLINE - /// - /// Insert the command executable path as the first element of the args array. - /// - /// - /// - private static string[] ConstructArrayArg(string[] args) - { - string[] newArgArray = new string[args.Length + 1]; - - newArgArray[0] = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - Array.Copy(args, 0, newArgArray, 1, args.Length); - - return newArgArray; - } -#endif // !FEATURE_GET_COMMANDLINE - /// /// Append output file with elapsedTime /// @@ -623,12 +599,7 @@ private static void DebuggerLaunchCheck() /// is ignored. /// A value of type ExitType that indicates whether the build succeeded, /// or the manner in which it failed. - public static ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + public static ExitType Execute(string[] commandLine) { DebuggerLaunchCheck(); @@ -645,9 +616,7 @@ public static ExitType Execute( // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); -#if FEATURE_GET_COMMANDLINE ErrorUtilities.VerifyThrowArgumentLength(commandLine); -#endif AppDomain.CurrentDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler; @@ -659,14 +628,11 @@ public static ExitType Execute( TextWriter targetsWriter = null; try { -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStart(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStart(string.Join(" ", commandLine)); } -#endif + Console.CancelKeyPress += cancelHandler; // check the operating system the code is running on @@ -722,7 +688,7 @@ public static ExitType Execute( bool reportFileAccesses = false; #endif - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _); + commandLineParser.GatherAllSwitches(commandLine, s_globalMessagesToLogInBuildLoggers, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _, out s_exeName); bool buildCanBeInvoked = ProcessCommandLineSwitches( switchesFromAutoResponseFile, @@ -769,11 +735,7 @@ public static ExitType Execute( ref getTargetResult, ref getResultOutputFile, recursing: false, -#if FEATURE_GET_COMMANDLINE - commandLine); -#else - string.Join(' ', commandLine)); -#endif + string.Join(" ", commandLine)); CommandLineSwitches.SwitchesFromResponseFiles = null; @@ -1007,12 +969,12 @@ public static ExitType Execute( exitType = ExitType.InitializationError; } } -#pragma warning disable CS0618 // Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now - catch (Exception e) when (e is ProjectCacheException || e is Experimental.ProjectCache.ProjectCacheException) +#pragma warning disable CS0618 // Microsoft.Build.Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now + catch (Exception e) when (e is ProjectCacheException || e is Microsoft.Build.Experimental.ProjectCache.ProjectCacheException) { ProjectCacheException pce = e as ProjectCacheException; - Experimental.ProjectCache.ProjectCacheException exppce = e as Experimental.ProjectCache.ProjectCacheException; + Microsoft.Build.Experimental.ProjectCache.ProjectCacheException exppce = e as Microsoft.Build.Experimental.ProjectCache.ProjectCacheException; Console.WriteLine($"MSBUILD : error {pce?.ErrorCode ?? exppce?.ErrorCode}: {e.Message}"); @@ -1073,14 +1035,10 @@ public static ExitType Execute( preprocessWriter?.Dispose(); targetsWriter?.Dispose(); -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStop(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStop(string.Join(" ", commandLine)); } -#endif } /********************************************************************************************************************** * WARNING: Do NOT add any more catch blocks above! @@ -1221,14 +1179,7 @@ private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs /// private static void ResetBuildState() { - ResetGatheringSwitchesState(); - } - - private static void ResetGatheringSwitchesState() - { - s_includedResponseFiles = new List(); - usingSwitchesFromAutoResponseFile = false; - CommandLineSwitches.SwitchesFromResponseFiles = new(); + commandLineParser.ResetGatheringSwitchesState(); } /// @@ -1303,11 +1254,7 @@ internal static bool BuildProject( #if FEATURE_REPORTFILEACCESSES bool reportFileAccesses, #endif -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else string[] commandLine) -#endif { if (FileUtilities.IsVCProjFilename(projectFile) || FileUtilities.IsDspFilename(projectFile)) { @@ -1392,8 +1339,8 @@ internal static bool BuildProject( // all of the loggers that are single-node only .. loggers, // all of the central loggers for multi-node systems. These need to be resilient to multiple calls - // to Initialize - .. distributedLoggerRecords.Select(d => d.CentralLogger) + // to Initialize. Filter out null loggers (e.g., DistributedFileLogger uses null central logger). + .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not null) ]; projectCollection = new ProjectCollection( @@ -1556,15 +1503,12 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) if (!Traits.Instance.EscapeHatches.DoNotSendDeferredMessagesToBuildManager) { var commandLineString = -#if FEATURE_GET_COMMANDLINE - commandLine; -#else string.Join(" ", commandLine); -#endif + messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLineString)); // Log a message for every response file and include it in log - foreach (var responseFilePath in s_includedResponseFiles) + foreach (var responseFilePath in commandLineParser.IncludedResponseFiles) { messagesToLogInBuildLoggers.Add( new BuildManager.DeferredBuildMessage( @@ -1931,23 +1875,26 @@ internal static void SetConsoleUI() CultureInfo.CurrentUICulture = desiredCulture; CultureInfo.DefaultThreadCurrentUICulture = desiredCulture; + if (!Traits.Instance.ConsoleUseDefaultEncoding) + { #if RUNTIME_TYPE_NETCORE - if (EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding()) + if (EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding()) #else - if (EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding() - && !CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase)) + if (EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding() + && !CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase)) #endif - { - try - { - // Setting both encodings causes a change in the CHCP, making it so we don't need to P-Invoke CHCP ourselves. - Console.OutputEncoding = Encoding.UTF8; - // If the InputEncoding is not set, the encoding will work in CMD but not in PowerShell, as the raw CHCP page won't be changed. - Console.InputEncoding = Encoding.UTF8; - } - catch (Exception ex) when (ex is IOException || ex is SecurityException) { - // The encoding is unavailable. Do nothing. + try + { + // Setting both encodings causes a change in the CHCP, making it so we don't need to P-Invoke CHCP ourselves. + Console.OutputEncoding = Encoding.UTF8; + // If the InputEncoding is not set, the encoding will work in CMD but not in PowerShell, as the raw CHCP page won't be changed. + Console.InputEncoding = Encoding.UTF8; + } + catch (Exception ex) when (ex is IOException || ex is SecurityException) + { + // The encoding is unavailable. Do nothing. + } } } @@ -1984,514 +1931,11 @@ internal static void SetConsoleUI() #endif } - /// - /// Gets all specified switches, from the command line, as well as all - /// response files, including the auto-response file. - /// - /// - /// - /// - /// - /// Combined bag of switches. - private static void GatherAllSwitches( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - out CommandLineSwitches switchesFromAutoResponseFile, out CommandLineSwitches switchesNotFromAutoResponseFile, out string fullCommandLine) - { - ResetGatheringSwitchesState(); - -#if FEATURE_GET_COMMANDLINE - // split the command line on (unquoted) whitespace - var commandLineArgs = QuotingUtilities.SplitUnquoted(commandLine); - - s_exeName = FileUtilities.FixFilePath(QuotingUtilities.Unquote(commandLineArgs[0])); -#else - var commandLineArgs = new List(commandLine); - - s_exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; -#endif - -#if USE_MSBUILD_DLL_EXTN - var msbuildExtn = ".dll"; -#else - var msbuildExtn = ".exe"; -#endif - if (!s_exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) - { - s_exeName += msbuildExtn; - } - - // discard the first piece, because that's the path to the executable -- the rest are args - commandLineArgs.RemoveAt(0); - -#if FEATURE_GET_COMMANDLINE - fullCommandLine = $"'{commandLine}'"; -#else - fullCommandLine = $"'{string.Join(' ', commandLine)}'"; -#endif - - // parse the command line, and flag syntax errors and obvious switch errors - switchesNotFromAutoResponseFile = new CommandLineSwitches(); - GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile, fullCommandLine); - - // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the - // switches on the command line - switchesFromAutoResponseFile = new CommandLineSwitches(); - if (!switchesNotFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - GatherAutoResponseFileSwitches(s_exePath, switchesFromAutoResponseFile, fullCommandLine); - } - } - - /// - /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and - /// flags syntax errors, and other obvious switch errors. - /// - /// - /// Internal for unit testing only. - /// - internal static void GatherCommandLineSwitches(List commandLineArgs, CommandLineSwitches commandLineSwitches, string commandLine = "") - { - foreach (string commandLineArg in commandLineArgs) - { - string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - - if (unquotedCommandLineArg.Length > 0) - { - // response file switch starts with @ - if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) - { - GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches, commandLine); - } - else - { - string switchName; - string switchParameters; - - // all switches should start with - or / or -- unless a project is being specified - if (!ValidateSwitchIndicatorInUnquotedArgument(unquotedCommandLineArg) || FileUtilities.LooksLikeUnixFilePath(unquotedCommandLineArg)) - { - switchName = null; - // add a (fake) parameter indicator for later parsing - switchParameters = $":{commandLineArg}"; - } - else - { - // check if switch has parameters (look for the : parameter indicator) - int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); - - // get the length of the beginning sequence considered as a switch indicator (- or / or --) - int switchIndicatorsLength = GetLengthOfSwitchIndicator(unquotedCommandLineArg); - - // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the - // leading - or / or --) and the parameter indicator (if the switch has parameters); the parameters (if any) - // follow the parameter indicator - if (switchParameterIndicator == -1) - { - switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength); - switchParameters = string.Empty; - } - else - { - switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength, switchParameterIndicator - switchIndicatorsLength); - switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator, switchIndicatorsLength); - } - } - - // Special case: for the switches "/m" (or "/maxCpuCount") and "/bl" (or "/binarylogger") we wish to pretend we saw a default argument - // This allows a subsequent /m:n on the command line to override it. - // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. - // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu - // for backwards compatibility. - if (string.IsNullOrEmpty(switchParameters)) - { - if (string.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) - { - int numberOfCpus = NativeMethodsShared.GetLogicalCoreCount(); - switchParameters = $":{numberOfCpus}"; - } - else if (string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase)) - { - // we have to specify at least one parameter otherwise it's impossible to distinguish the situation - // where /bl is not specified at all vs. where /bl is specified without the file name. - switchParameters = ":msbuild.binlog"; - } - else if (string.Equals(switchName, "prof", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "profileevaluation", StringComparison.OrdinalIgnoreCase)) - { - switchParameters = ":no-file"; - } - } - - if (CommandLineSwitches.IsParameterlessSwitch(switchName, out var parameterlessSwitch, out var duplicateSwitchErrorMessage)) - { - GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out var parameterizedSwitch, out duplicateSwitchErrorMessage, out var multipleParametersAllowed, out var missingParametersErrorMessage, out var unquoteParameters, out var allowEmptyParameters)) - { - GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg, allowEmptyParameters, commandLine); - } - else - { - commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg, commandLine); - } - } - } - } - } - - /// - /// Extracts a switch's parameters after processing all quoting around the switch. - /// - /// - /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". - /// - /// - /// - /// - /// - /// - /// - /// The given switch's parameters (with interesting quoting preserved). - internal static string ExtractSwitchParameters( - string commandLineArg, - string unquotedCommandLineArg, - int doubleQuotesRemovedFromArg, - string switchName, - int switchParameterIndicator, - int switchIndicatorsLength) - { - - // find the parameter indicator again using the quoted arg - // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a - // parameter indicator cannot be escaped or made into a literal - int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); - - // check if there is any quoting in the name portion of the switch - string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); - - ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), - "The switch name extracted from either the partially or completely unquoted arg should be the same."); - - ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, - "The name portion of the switch cannot contain more quoting than the arg itself."); - - string switchParameters; - // if quoting in the name portion of the switch was terminated - if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) - { - // get the parameters exactly as specified on the command line i.e. including quoting - switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); - } - else - { - // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) - // terminates the switch parameters - int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); - if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && - ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) - { - // then the parameters are not quoted in any interesting way, so use the unquoted parameters - switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); - } - else - { - // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name - // portion of the switch - switchParameters = $":\"{commandLineArg.Substring(quotedSwitchParameterIndicator + 1)}"; - } - } - - ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); - - return switchParameters; - } - - /// - /// Used to keep track of response files to prevent them from - /// being included multiple times (or even recursively). - /// - private static List s_includedResponseFiles; - - /// - /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses - /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. - /// - /// - /// - private static void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches, string commandLine) - { - try - { - string responseFile = FileUtilities.FixFilePath(unquotedCommandLineArg.Substring(1)); - - if (responseFile.Length == 0) - { - commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg, commandLine); - } - else if (!FileSystems.Default.FileExists(responseFile)) - { - commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg, commandLine); - } - else - { - // normalize the response file path to help catch multiple (or recursive) inclusions - responseFile = Path.GetFullPath(responseFile); - // NOTE: for network paths or mapped paths, normalization is not guaranteed to work - - bool isRepeatedResponseFile = false; - - foreach (string includedResponseFile in s_includedResponseFiles) - { - if (string.Equals(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase)) - { - commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg, commandLine); - isRepeatedResponseFile = true; - break; - } - } - - if (!isRepeatedResponseFile) - { - var responseFileDirectory = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(responseFile)); - s_includedResponseFiles.Add(responseFile); - - List argsFromResponseFile; - -#if FEATURE_ENCODING_DEFAULT - using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. -#else - using (StreamReader responseFileContents = FileUtilities.OpenRead(responseFile)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. -#endif - { - argsFromResponseFile = new List(); - - while (responseFileContents.Peek() != -1) - { - // ignore leading whitespace on each line - string responseFileLine = responseFileContents.ReadLine().TrimStart(); - - // skip comment lines beginning with # - if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) - { - // Allow special case to support a path relative to the .rsp file being processed. - responseFileLine = Regex.Replace(responseFileLine, responseFilePathReplacement, - responseFileDirectory, RegexOptions.IgnoreCase); - - // treat each line of the response file like a command line i.e. args separated by whitespace - argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); - } - } - } - - CommandLineSwitches.SwitchesFromResponseFiles.Add((responseFile, string.Join(" ", argsFromResponseFile))); - - GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches, commandLine); - } - } - } - catch (NotSupportedException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (SecurityException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (UnauthorizedAccessException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (IOException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - } - - /// - /// Called when a switch that doesn't take parameters is detected on the command line. - /// - /// - /// - /// - /// - /// - private static void GatherParameterlessCommandLineSwitch( - CommandLineSwitches commandLineSwitches, - CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, - string switchParameters, - string duplicateSwitchErrorMessage, - string unquotedCommandLineArg, - string commandLine) - { - // switch should not have any parameters - if (switchParameters.Length == 0) - { - // check if switch is duplicated, and if that's allowed - if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || - (duplicateSwitchErrorMessage == null)) - { - commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); - } - else - { - commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - } - else - { - commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg, commandLine); - } - } - - /// - /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the - /// switch parameters. - /// - /// - /// - /// - /// - /// - /// - /// - /// - private static void GatherParameterizedCommandLineSwitch( - CommandLineSwitches commandLineSwitches, - CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, - string switchParameters, - string duplicateSwitchErrorMessage, - bool multipleParametersAllowed, - string missingParametersErrorMessage, - bool unquoteParameters, - string unquotedCommandLineArg, - bool allowEmptyParameters, - string commandLine) - { - if (// switch must have parameters - (switchParameters.Length > 1) || - // unless the parameters are optional - (missingParametersErrorMessage == null)) - { - // skip the parameter indicator (if any) - if (switchParameters.Length > 0) - { - switchParameters = switchParameters.Substring(1); - } - - if (parameterizedSwitch == CommandLineSwitches.ParameterizedSwitch.Project && IsEnvironmentVariable(switchParameters)) - { - commandLineSwitches.SetSwitchError("EnvironmentVariableAsSwitch", unquotedCommandLineArg, commandLine); - } - - // check if switch is duplicated, and if that's allowed - if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || - (duplicateSwitchErrorMessage == null)) - { - // save the parameters after unquoting and splitting them if necessary - if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters, allowEmptyParameters)) - { - // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional - if (missingParametersErrorMessage != null) - { - commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); - } - } - } - else - { - commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - } - else - { - commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); - } - } - - /// - /// Checks whether envVar is an environment variable. MSBuild uses - /// Environment.ExpandEnvironmentVariables(string), which only - /// considers %-delimited variables. - /// - /// A possible environment variable - /// Whether envVar is an environment variable - private static bool IsEnvironmentVariable(string envVar) - { - return envVar.StartsWith("%") && envVar.EndsWith("%") && envVar.Length > 1; - } - - /// - /// The name of the auto-response file. - /// - private const string autoResponseFileName = "MSBuild.rsp"; - - /// - /// The name of an auto-response file to search for in the project directory and above. - /// - private const string directoryResponseFileName = "Directory.Build.rsp"; - - /// - /// String replacement pattern to support paths in response files. - /// - private const string responseFilePathReplacement = "%MSBuildThisFileDirectory%"; - - /// - /// Whether switches from the auto-response file are being used. - /// - internal static bool usingSwitchesFromAutoResponseFile = false; - /// /// Indicates that this process is working as a server. /// private static bool s_isServerNode; - /// - /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the - /// switches from the auto-response file with the switches passed in. - /// Returns true if the response file was found. - /// - private static bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) - { - string autoResponseFile = Path.Combine(path, autoResponseFileName); - return GatherAutoResponseFileSwitchesFromFullPath(autoResponseFile, switchesFromAutoResponseFile, commandLine); - } - - private static bool GatherAutoResponseFileSwitchesFromFullPath(string autoResponseFile, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) - { - bool found = false; - - // if the auto-response file does not exist, only use the switches on the command line - if (FileSystems.Default.FileExists(autoResponseFile)) - { - found = true; - GatherResponseFileSwitch($"@{autoResponseFile}", switchesFromAutoResponseFile, commandLine); - - // if the "/noautoresponse" switch was set in the auto-response file, flag an error - if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", - switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse), commandLine); - } - - if (switchesFromAutoResponseFile.HaveAnySwitchesBeenSet()) - { - // we picked up some switches from the auto-response file - usingSwitchesFromAutoResponseFile = true; - } - - // Throw errors found in the response file - switchesFromAutoResponseFile.ThrowErrors(); - } - - return found; - } - /// /// Coordinates the processing of all detected switches. It gathers information necessary to invoke the build engine, and /// performs deeper error checking on the switches and their parameters. @@ -2567,8 +2011,8 @@ private static bool ProcessCommandLineSwitches( bool useTerminalLogger = ProcessTerminalLoggerConfiguration(commandLineSwitches, out string aggregatedTerminalLoggerParameters); // This is temporary until we can remove the need for the environment variable. - // DO NOT use this environment variable for any new features as it will be removed without further notice. - Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); + // DO NOT use this environment variable for any new features as it will be removed without further notice. + Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); DisplayVersionMessageIfNeeded(recursing, useTerminalLogger, commandLineSwitches); @@ -2629,7 +2073,7 @@ private static bool ProcessCommandLineSwitches( } else { - bool foundProjectAutoResponseFile = CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); + bool foundProjectAutoResponseFile = commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); if (foundProjectAutoResponseFile) { @@ -3088,59 +2532,6 @@ private static CommandLineSwitches CombineSwitchesRespectingPriority(CommandLine return commandLineSwitches; } - private static string GetProjectDirectory(string[] projectSwitchParameters) - { - string projectDirectory = "."; - ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); - - if (projectSwitchParameters.Length == 1) - { - var projectFile = FileUtilities.FixFilePath(projectSwitchParameters[0]); - - if (FileSystems.Default.DirectoryExists(projectFile)) - { - // the provided argument value is actually the directory - projectDirectory = projectFile; - } - else - { - InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); - projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); - } - } - - return projectDirectory; - } - - - /// - /// Identifies if there is rsp files near the project file - /// - /// true if there autoresponse file was found - private static bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) - { - bool found = false; - - var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); - - if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - // gather any switches from the first Directory.Build.rsp found in the project directory or above - string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); - - found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); - - // Don't look for more response files if it's only in the same place we already looked (next to the exe) - if (!string.Equals(projectDirectory, s_exePath, StringComparison.OrdinalIgnoreCase)) - { - // this combines any found, with higher precedence, with the switches from the original auto response file switches - found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); - } - } - - return found; - } - private static bool WarningsAsErrorsSwitchIsEmpty(CommandLineSwitches commandLineSwitches) { string val = commandLineSwitches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.WarningsAsErrors); @@ -3231,6 +2622,37 @@ private static string[] ProcessInputResultsCaches(CommandLineSwitches commandLin : null; } + /// + /// Processes the parent packet version switch, which indicates the packet version supported by the parent node. + /// This is used for backward-compatible packet version negotiation between parent and child nodes. + /// If not specified, returns 1 indicating the parent doesn't support version negotiation (old parent). + /// + /// The command line parameters for the switch. + /// The packet version supported by the parent, or 1 if not specified or invalid. + internal static byte ProcessParentPacketVersionSwitch(string[] parameters) + { + byte parentPacketVersion = 1; + + if (parameters.Length > 0) + { + try + { + parentPacketVersion = byte.Parse(parameters[parameters.Length - 1], CultureInfo.InvariantCulture); + } + catch (FormatException ex) + { + CommunicationsUtilities.Trace("Invalid node packet version value '{0}': {1}", parameters[parameters.Length - 1], ex.Message); + } + catch (OverflowException ex) + { + // Value too large for byte - log and continue with default + CommunicationsUtilities.Trace("Node packet version value '{0}' out of range: {1}", parameters[parameters.Length - 1], ex.Message); + } + } + + return parentPacketVersion; + } + /// /// Processes the node reuse switch, the user can set node reuse to true, false or not set the switch. If the switch is /// not set the system will check to see if the process is being run as an administrator. This check in localnode provider @@ -3483,8 +2905,9 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool { // We now have an option to run a long-lived sidecar TaskHost so we have to handle the NodeReuse switch. bool nodeReuse = ProcessNodeReuseSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeReuse]); - OutOfProcTaskHostNode node = new OutOfProcTaskHostNode(); - shutdownReason = node.Run(out nodeException, nodeReuse); + byte parentPacketVersion = ProcessParentPacketVersionSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ParentPacketVersion]); + OutOfProcTaskHostNode node = new(); + shutdownReason = node.Run(out nodeException, nodeReuse, parentPacketVersion); } else if (nodeModeNumber == 3) { @@ -3599,7 +3022,7 @@ internal static string ProcessProjectSwitch( if (parameters.Length == 1) { - projectFile = FileUtilities.FixFilePath(parameters[0]); + projectFile = FrameworkFileUtilities.FixFilePath(parameters[0]); if (FileSystems.Default.DirectoryExists(projectFile)) { @@ -3743,46 +3166,6 @@ private static void ValidateExtensions(string[] projectExtensionsToIgnore) } } - /// - /// Checks whether an argument given as a parameter starts with valid indicator, - ///
which means, whether switch begins with one of: "/", "-", "--" - ///
- /// Command line argument with beginning indicator (e.g. --help). - ///
This argument has to be unquoted, otherwise the first character will always be a quote character " - /// true if argument's beginning matches one of possible indicators - ///
false if argument's beginning doesn't match any of correct indicator - ///
- private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) - { - return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" - || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); - } - - /// - /// Gets the length of the switch indicator (- or / or --) - ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. - ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. - ///
- /// Unquoted argument with leading indicator and name - /// Correct length of used indicator - ///
0 if no leading sequence recognized as correct indicator
- /// Internal for testing purposes - internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) - { - if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) - { - return 2; - } - else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) - { - return 1; - } - else - { - return 0; - } - } - /// /// Figures out which targets are to be built. /// @@ -4040,16 +3423,40 @@ private static void ProcessBinaryLogger(string[] binaryLoggerParameters, List 0) + { + Console.WriteLine(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("DuplicateBinaryLoggerPathsIgnored", string.Join(", ", processedParams.DuplicateFilePaths))); + } + + if (processedParams.AllConfigurationsIdentical && processedParams.AdditionalFilePaths.Count > 0) + { + // Optimized approach: single logger writing to one file, then copy to additional locations + BinaryLogger logger = new() { Parameters = processedParams.DistinctParameterSets[0], AdditionalFilePaths = processedParams.AdditionalFilePaths }; + loggers.Add(logger); + } + else + { + // Create separate logger instances for each distinct configuration + foreach (string paramSet in processedParams.DistinctParameterSets) + { + BinaryLogger logger = new BinaryLogger { Parameters = paramSet }; + loggers.Add(logger); + } + } } /// @@ -4201,7 +3608,7 @@ internal static void ProcessDistributedFileLogger( // Check to see if the logfile parameter has been set, if not set it to the current directory string logFileParameter = ExtractAnyLoggerParameter(fileParameters, "logfile"); - string logFileName = FileUtilities.FixFilePath(ExtractAnyParameterValue(logFileParameter)); + string logFileName = FrameworkFileUtilities.FixFilePath(ExtractAnyParameterValue(logFileParameter)); try { @@ -4465,7 +3872,7 @@ private static LoggerDescription ParseLoggingParameter(string parameter, string } // figure out whether the assembly's identity (strong/weak name), or its filename/path is provided - string testFile = FileUtilities.FixFilePath(loggerAssemblySpec); + string testFile = FrameworkFileUtilities.FixFilePath(loggerAssemblySpec); if (FileSystems.Default.FileExists(testFile)) { loggerAssemblyFile = testFile; @@ -4623,7 +4030,7 @@ private static string ProcessValidateSwitch(string[] parameters) foreach (string parameter in parameters) { InitializationException.VerifyThrow(schemaFile == null, "MultipleSchemasError", parameter); - string fileName = FileUtilities.FixFilePath(parameter); + string fileName = FrameworkFileUtilities.FixFilePath(parameter); InitializationException.VerifyThrow(FileSystems.Default.FileExists(fileName), "SchemaNotFoundError", fileName); schemaFile = Path.Combine(Directory.GetCurrentDirectory(), fileName); diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 9bf8b014e38..6393b8d80d9 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -57,26 +57,28 @@ - - + + - - + + + - - + + + - - + + - - + + @@ -94,162 +96,81 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 9c41d0b862c..52243986175 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -33,15 +33,11 @@ - + - - - - - + @@ -57,71 +53,71 @@ - + - + - + - - - - - + - + - + - + - + - + - + - + - + + + + + - + - + - + diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj index 66d83585f5d..a514cbcdcbf 100644 --- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj +++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -88,9 +88,12 @@ ExceptionHandling.cs - + FileUtilities.cs + + SharedFileUtilities.cs + FileUtilitiesRegex.cs diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 3b75caa7fb1..9b44bb3f248 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -26,7 +26,7 @@ folder InstallDir:\MSBuild\Current folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.Build.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 - file source=$(X86BinPath)Microsoft.Build.Framework.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all + file source=$(X86BinPath)Microsoft.Build.Framework.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenApplications="[installDir]\Common7\IDE\VcxprojReader.exe" vs.file.ngenArchitecture=all file source=$(X86BinPath)Microsoft.Build.Framework.tlb file source=$(X86BinPath)Microsoft.Build.Tasks.Core.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)Microsoft.Build.Utilities.Core.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 @@ -41,7 +41,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.VisualStudio.SolutionPersistence.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)RuntimeContracts.dll file source=$(X86BinPath)System.Buffers.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 - file source=$(X86BinPath)System.Diagnostics.DiagnosticSource.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)System.Formats.Nrbf.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.IO.Pipelines.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Memory.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 @@ -52,7 +51,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.Bcl.AsyncInterfaces.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Text.Encodings.Web.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Threading.Tasks.Extensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 - file source=$(X86BinPath)System.ValueTuple.dll file source=$(X86BinPath)System.Numerics.Vectors.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Resources.Extensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)System.Runtime.CompilerServices.Unsafe.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 @@ -89,24 +87,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.ServiceModel.targets file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets - file source=$(X86BinPath)Microsoft.VisualStudio.OpenTelemetry.ClientExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.VisualStudio.OpenTelemetry.Collector.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.Api.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.Api.ProviderBuilderExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Binder.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.ConfigurationExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Primitives.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Diagnostics.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Newtonsoft.Json.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 folder InstallDir:\MSBuild\Current\Bin\MSBuild @@ -221,7 +202,6 @@ folder InstallDir:\MSBuild\Current\Bin\amd64 file source=$(X86BinPath)Microsoft.IO.Redist.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Text.Encodings.Web.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Threading.Tasks.Extensions.dll vs.file.ngenArchitecture=all - file source=$(X86BinPath)System.ValueTuple.dll file source=$(X86BinPath)System.Numerics.Vectors.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Resources.Extensions.dll file source=$(X86BinPath)System.Runtime.CompilerServices.Unsafe.dll vs.file.ngenArchitecture=all diff --git a/src/Package/Microsoft.Build.UnGAC/Program.cs b/src/Package/Microsoft.Build.UnGAC/Program.cs index a13f518146d..d686da3dc75 100644 --- a/src/Package/Microsoft.Build.UnGAC/Program.cs +++ b/src/Package/Microsoft.Build.UnGAC/Program.cs @@ -32,8 +32,6 @@ private static void Main(string[] args) "BuildXL.Utilities.Core, Version=1.0.0.0", "BuildXL.Native, Version=1.0.0.0", "Microsoft.VisualStudio.SolutionPersistence, Version=1.0.0.0", - "Microsoft.VisualStudio.OpenTelemetry.ClientExtensions, Version=0.1.0.0", - "Microsoft.VisualStudio.OpenTelemetry.Collector, Version=0.1.0.0", }; uint hresult = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index f17d95b612e..119464d22dd 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -18,6 +18,7 @@ using System.Threading; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; #if !CLR2COMPATIBILITY using Microsoft.Build.Shared.Debugging; @@ -107,16 +108,46 @@ internal enum HandshakeOptions /// internal enum HandshakeStatus { + /// + /// The handshake operation completed successfully. + /// Success = 0, - // Other node resurned different value than we expected. - // This can happen either by attempting to connect to a wrong node type. (e.g. transient TaskHost trying to connect to a long-running TaskHost) - // Or by trying to connect to a node that has a different MSBuild version. + + /// + /// The other node returned a different value than expected. + /// This can happen either by attempting to connect to a wrong node type + /// (e.g., transient TaskHost trying to connect to a long-running TaskHost) + /// or by trying to connect to a node that has a different MSBuild version. + /// VersionMismatch = 1, - // TryReadInt -> Abort due to old MSBuild version + + /// + /// The handshake was aborted due to connection from an old MSBuild version. + /// Occurs in TryReadInt when detecting legacy MSBuild.exe connections. + /// OldMSBuild = 2, + + /// + /// The handshake operation timed out before completion. + /// Timeout = 3, + + /// + /// The stream ended unexpectedly during the handshake operation. + /// Indicates an incomplete or corrupted handshake sequence. + /// UnexpectedEndOfStream = 4, + + /// + /// The endianness (byte order) of the communicating nodes does not match. + /// Indicates an architecture compatibility issue. + /// EndiannessMismatch = 5, + + /// + /// The handshake status is undefined or uninitialized. + /// + Undefined, } /// @@ -141,25 +172,34 @@ internal class HandshakeResult /// public string ErrorMessage { get; } + /// + /// The negotiated packet version with the child node. + /// It's needed to ensure both sides of the communication can read/write data in pipe. + /// + public byte NegotiatedPacketVersion { get; } + /// /// Initializes a new instance of the class. /// /// The status of the handshake operation. /// The value returned from the handshake. /// The error message if the handshake failed. - private HandshakeResult(HandshakeStatus status, int value, string errorMessage) + /// The packet version from the child node. + private HandshakeResult(HandshakeStatus status, int value, string errorMessage, byte negotiatedPacketVersion = 1) { Status = status; Value = value; ErrorMessage = errorMessage; + NegotiatedPacketVersion = negotiatedPacketVersion; } /// /// Creates a successful handshake result with the specified value. /// /// The value returned from the handshake operation. + /// The packet version received from the child node. /// A new instance representing a successful operation. - public static HandshakeResult Success(int value = 0) => new(HandshakeStatus.Success, value, null); + public static HandshakeResult Success(int value = 0, byte negotiatedPacketVersion = 1) => new(HandshakeStatus.Success, value, null, negotiatedPacketVersion); /// /// Creates a failed handshake result with the specified status and error message. @@ -172,11 +212,14 @@ private HandshakeResult(HandshakeStatus status, int value, string errorMessage) internal class Handshake { + /// + /// Marker indicating that the next integer in the child handshake response is the PacketVersion. + /// + public const int PacketVersionFromChildMarker = -1; + // The number is selected as an arbitrary value that is unlikely to conflict with any future sdk version. public const int NetTaskHostHandshakeVersion = 99; - public const HandshakeOptions NetTaskHostFlags = HandshakeOptions.NET | HandshakeOptions.TaskHost; - protected readonly HandshakeComponents _handshakeComponents; /// @@ -214,7 +257,7 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string pre CommunicationsUtilities.Trace("Building handshake for node type {0}, (version {1}): options {2}.", nodeType, handshakeVersion, options); // Calculate salt from environment and tools directory - bool isNetTaskHost = IsHandshakeOptionEnabled(nodeType, NetTaskHostFlags); + bool isNetTaskHost = IsHandshakeOptionEnabled(nodeType, HandshakeOptions.NET | HandshakeOptions.TaskHost); string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? ""; string toolsDirectory = GetToolsDirectory(isNetTaskHost, predefinedToolsDirectory); int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}"); @@ -731,24 +774,53 @@ internal static bool TryReadEndOfHandshakeSignal( #endif out HandshakeResult innerResult)) { + byte negotiatedPacketVersion = 1; + if (innerResult.Value != EndOfHandshakeSignal) { - if (isProvider) + // If the received handshake part is not PacketVersionFromChildMarker it means we communicate with the host that does not support packet version negotiation. + // Fallback to the old communication validation pattern. + if (innerResult.Value != Handshake.PacketVersionFromChildMarker) { - var errorMessage = $"Handshake failed on part {innerResult.Value}. Probably the client is a different MSBuild build."; - Trace(errorMessage); - result = HandshakeResult.Failure(HandshakeStatus.VersionMismatch, errorMessage); + result = CreateVersionMismatchResult(isProvider, innerResult.Value); return false; } - else + + // We detected packet version marker, now let's read actual PacketVersion + if (!stream.TryReadIntForHandshake( + byteToAccept: null, +#if NETCOREAPP2_1_OR_GREATER + timeout, +#endif + out HandshakeResult versionResult)) + { + result = versionResult; + return false; + } + + byte childVersion = (byte)versionResult.Value; + negotiatedPacketVersion = NodePacketTypeExtensions.GetNegotiatedPacketVersion(childVersion); + Trace("Node PacketVersion: {0}, Local: {1}, Negotiated: {2}", childVersion, NodePacketTypeExtensions.PacketVersion, negotiatedPacketVersion); + + if (!stream.TryReadIntForHandshake( + byteToAccept: null, +#if NETCOREAPP2_1_OR_GREATER + timeout, +#endif + out innerResult)) + { + result = innerResult; + return false; + } + + if (innerResult.Value != EndOfHandshakeSignal) { - var errorMessage = $"Expected end of handshake signal but received {innerResult.Value}. Probably the host is a different MSBuild build."; - Trace(errorMessage); - result = HandshakeResult.Failure(HandshakeStatus.VersionMismatch, errorMessage); + result = CreateVersionMismatchResult(isProvider, innerResult.Value); return false; } } - result = HandshakeResult.Success(0); + + result = HandshakeResult.Success(0, negotiatedPacketVersion); return true; } else @@ -758,12 +830,24 @@ internal static bool TryReadEndOfHandshakeSignal( } } + private static HandshakeResult CreateVersionMismatchResult(bool isProvider, int receivedValue) + { + var errorMessage = isProvider + ? $"Handshake failed on part {receivedValue}. Probably the client is a different MSBuild build." + : $"Expected end of handshake signal but received {receivedValue}. Probably the host is a different MSBuild build."; + Trace(errorMessage); + + return HandshakeResult.Failure(HandshakeStatus.VersionMismatch, errorMessage); + } + #pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter /// /// Extension method to read a series of bytes from a stream. /// If specified, leading byte matches one in the supplied array if any, returns rejection byte and throws IOException. /// - internal static bool TryReadIntForHandshake(this PipeStream stream, byte? byteToAccept, + internal static bool TryReadIntForHandshake( + this PipeStream stream, + byte? byteToAccept, #if NETCOREAPP2_1_OR_GREATER int timeout, #endif diff --git a/src/Shared/FileMatcher.cs b/src/Shared/FileMatcher.cs index 584d0b955d9..d08e6549791 100644 --- a/src/Shared/FileMatcher.cs +++ b/src/Shared/FileMatcher.cs @@ -46,7 +46,7 @@ internal class FileMatcher #endif // on OSX both System.IO.Path separators are '/', so we have to use the literals - internal static readonly char[] directorySeparatorCharacters = FileUtilities.Slashes; + internal static readonly char[] directorySeparatorCharacters = FrameworkFileUtilities.Slashes; // until Cloudbuild switches to EvaluationContext, we need to keep their dependence on global glob caching via an environment variable private static readonly Lazy>> s_cachedGlobExpansions = new Lazy>>(() => new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase)); @@ -233,7 +233,7 @@ internal static bool HasPropertyOrItemReferences(string filespec) /// private static IReadOnlyList GetAccessibleFileSystemEntries(IFileSystem fileSystem, FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) { - path = FileUtilities.FixFilePath(path); + path = FrameworkFileUtilities.FixFilePath(path); switch (entityType) { case FileSystemEntity.Files: return GetAccessibleFiles(fileSystem, path, pattern, projectDirectory, stripProjectDirectory); @@ -592,7 +592,7 @@ private static void PreprocessFileSpecForSplitting( out string wildcardDirectoryPart, out string filenamePart) { - filespec = FileUtilities.FixFilePath(filespec); + filespec = FrameworkFileUtilities.FixFilePath(filespec); int indexOfLastDirectorySeparator = filespec.LastIndexOfAny(directorySeparatorCharacters); if (-1 == indexOfLastDirectorySeparator) { @@ -2145,7 +2145,7 @@ private SearchAction GetFileSearchData( wildcard[wildcardLength - 1] == '*') { // Check that there are no other slashes in the wildcard. - if (wildcard.IndexOfAny(FileUtilities.Slashes, 3, wildcardLength - 6) == -1) + if (wildcard.IndexOfAny(FrameworkFileUtilities.Slashes, 3, wildcardLength - 6) == -1) { directoryPattern = wildcard.Substring(3, wildcardLength - 6); } @@ -2679,7 +2679,7 @@ private static bool IsSubdirectoryOf(string possibleChild, string possibleParent /// True in case of a match (e.g. directoryPath = "dir/subdir" and pattern = "s*"), false otherwise. private static bool DirectoryEndsWithPattern(string directoryPath, string pattern) { - int index = directoryPath.LastIndexOfAny(FileUtilities.Slashes); + int index = directoryPath.LastIndexOfAny(FrameworkFileUtilities.Slashes); return (index != -1 && IsMatch(directoryPath.AsSpan(index + 1), pattern)); } diff --git a/src/Shared/FileUtilities.cs b/src/Shared/FileUtilities.cs index 34e36d5c3d3..0ca435b0137 100644 --- a/src/Shared/FileUtilities.cs +++ b/src/Shared/FileUtilities.cs @@ -143,8 +143,6 @@ public static bool GetIsFileSystemCaseSensitive() internal static char[] InvalidFileNameChars => InvalidFileNameCharsArray; #endif - internal static readonly char[] Slashes = { '/', '\\' }; - internal static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString(); private static readonly ConcurrentDictionary FileExistenceCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -240,39 +238,22 @@ internal static void ClearCacheDirectory() } } - /// - /// If the given path doesn't have a trailing slash then add one. - /// If the path is an empty string, does not modify it. - /// - /// The path to check. - /// A path with a slash. - internal static string EnsureTrailingSlash(string fileSpec) - { - fileSpec = FixFilePath(fileSpec); - if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1])) - { - fileSpec += Path.DirectorySeparatorChar; - } - - return fileSpec; - } - /// /// Ensures the path does not have a leading or trailing slash after removing the first 'start' characters. /// internal static string EnsureNoLeadingOrTrailingSlash(string path, int start) { int stop = path.Length; - while (start < stop && IsSlash(path[start])) + while (start < stop && FrameworkFileUtilities.IsSlash(path[start])) { start++; } - while (start < stop && IsSlash(path[stop - 1])) + while (start < stop && FrameworkFileUtilities.IsSlash(path[stop - 1])) { stop--; } - return FixFilePath(path.Substring(start, stop - start)); + return FrameworkFileUtilities.FixFilePath(path.Substring(start, stop - start)); } /// @@ -281,12 +262,12 @@ internal static string EnsureNoLeadingOrTrailingSlash(string path, int start) internal static string EnsureTrailingNoLeadingSlash(string path, int start) { int stop = path.Length; - while (start < stop && IsSlash(path[start])) + while (start < stop && FrameworkFileUtilities.IsSlash(path[start])) { start++; } - return FixFilePath(start < stop && IsSlash(path[stop - 1]) ? + return FrameworkFileUtilities.FixFilePath(start < stop && FrameworkFileUtilities.IsSlash(path[stop - 1]) ? path.Substring(start) : #if NET string.Concat(path.AsSpan(start), new(in Path.DirectorySeparatorChar))); @@ -295,20 +276,6 @@ internal static string EnsureTrailingNoLeadingSlash(string path, int start) #endif } - /// - /// Ensures the path does not have a trailing slash. - /// - internal static string EnsureNoTrailingSlash(string path) - { - path = FixFilePath(path); - if (EndsWithSlash(path)) - { - path = path.Substring(0, path.Length - 1); - } - - return path; - } - /// /// Ensures the path is enclosed within single quotes. /// @@ -337,7 +304,7 @@ internal static string EnsureDoubleQuotes(string path) /// The path enclosed by quotes. internal static string EnsureQuotes(string path, bool isSingleQuote = true) { - path = FixFilePath(path); + path = FrameworkFileUtilities.FixFilePath(path); const char singleQuote = '\''; const char doubleQuote = '\"'; @@ -365,28 +332,6 @@ internal static string EnsureQuotes(string path, bool isSingleQuote = true) return path; } - /// - /// Indicates if the given file-spec ends with a slash. - /// - /// The file spec. - /// true, if file-spec has trailing slash - internal static bool EndsWithSlash(string fileSpec) - { - return (fileSpec.Length > 0) - ? IsSlash(fileSpec[fileSpec.Length - 1]) - : false; - } - - /// - /// Indicates if the given character is a slash. - /// - /// - /// true, if slash - internal static bool IsSlash(char c) - { - return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar); - } - /// /// Trims the string and removes any double quotes around it. /// @@ -419,7 +364,7 @@ internal static String GetDirectoryNameOfFullPath(String fullPath) ; } - return FixFilePath(fullPath.Substring(0, i)); + return FrameworkFileUtilities.FixFilePath(fullPath.Substring(0, i)); } return null; } @@ -430,7 +375,7 @@ internal static string TruncatePathToTrailingSegments(string path, int trailingS ErrorUtilities.VerifyThrowInternalLength(path, nameof(path)); ErrorUtilities.VerifyThrow(trailingSegmentsToKeep >= 0, "trailing segments must be positive"); - var segments = path.Split(Slashes, StringSplitOptions.RemoveEmptyEntries); + var segments = path.Split(FrameworkFileUtilities.Slashes, StringSplitOptions.RemoveEmptyEntries); var headingSegmentsToRemove = Math.Max(0, segments.Length - trailingSegmentsToKeep); @@ -501,7 +446,7 @@ internal static string NormalizePath(string path) { ErrorUtilities.VerifyThrowArgumentLength(path); string fullPath = GetFullPath(path); - return FixFilePath(fullPath); + return FrameworkFileUtilities.FixFilePath(fullPath); } internal static string NormalizePath(string directory, string file) @@ -578,14 +523,9 @@ From Path.cs in the CLR } #endif // FEATURE_LEGACY_GETFULLPATH - internal static string FixFilePath(string path) - { - return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/'); // .Replace("//", "/"); - } - /// /// Normalizes all path separators (both forward and back slashes) to forward slashes. - /// This is platform-independent, unlike FixFilePath which only normalizes on non-Windows platforms. + /// This is platform-independent, unlike FrameworkFileUtilities.FixFilePath which only normalizes on non-Windows platforms. /// Use this when you need consistent path comparison regardless of which separator style is used. /// /// The path to normalize @@ -742,7 +682,7 @@ internal static bool LooksLikeUnixFilePath(ReadOnlySpan value, string base /// directory path internal static string GetDirectory(string fileSpec) { - string directory = Path.GetDirectoryName(FixFilePath(fileSpec)); + string directory = Path.GetDirectoryName(FrameworkFileUtilities.FixFilePath(fileSpec)); // if file-spec is a root directory e.g. c:, c:\, \, \\server\share // NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server @@ -751,7 +691,7 @@ internal static string GetDirectory(string fileSpec) // just use the file-spec as-is directory = fileSpec; } - else if ((directory.Length > 0) && !EndsWithSlash(directory)) + else if ((directory.Length > 0) && !FrameworkFileUtilities.EndsWithSlash(directory)) { // restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories) directory += Path.DirectorySeparatorChar; @@ -836,7 +776,7 @@ internal static bool HasExtension(string fileName, string[] allowedExtensions) internal static string GetFullPath(string fileSpec, string currentDirectory, bool escape = true) { // Sending data out of the engine into the filesystem, so time to unescape. - fileSpec = FixFilePath(EscapingUtilities.UnescapeAll(fileSpec)); + fileSpec = FrameworkFileUtilities.FixFilePath(EscapingUtilities.UnescapeAll(fileSpec)); string fullPath = NormalizePath(Path.Combine(currentDirectory, fileSpec)); // In some cases we might want to NOT escape in order to preserve symbols like @, %, $ etc. @@ -846,7 +786,7 @@ internal static string GetFullPath(string fileSpec, string currentDirectory, boo fullPath = EscapingUtilities.Escape(fullPath); } - if (NativeMethodsShared.IsWindows && !EndsWithSlash(fullPath)) + if (NativeMethodsShared.IsWindows && !FrameworkFileUtilities.EndsWithSlash(fullPath)) { if (FileUtilitiesRegex.IsDrivePattern(fileSpec) || FileUtilitiesRegex.IsUncPattern(fullPath)) @@ -929,13 +869,13 @@ internal static bool PathIsInvalid(string path) #if NET if (!path.AsSpan().ContainsAny(InvalidPathChars)) { - int lastDirectorySeparator = path.LastIndexOfAny(Slashes); + int lastDirectorySeparator = path.LastIndexOfAny(FrameworkFileUtilities.Slashes); return path.AsSpan(lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0).ContainsAny(InvalidFileNameChars); } #else if (path.IndexOfAny(InvalidPathChars) < 0) { - int lastDirectorySeparator = path.LastIndexOfAny(Slashes); + int lastDirectorySeparator = path.LastIndexOfAny(FrameworkFileUtilities.Slashes); return path.IndexOfAny(InvalidFileNameChars, lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0) >= 0; } #endif @@ -949,7 +889,7 @@ internal static void DeleteNoThrow(string path) { try { - File.Delete(FixFilePath(path)); + File.Delete(FrameworkFileUtilities.FixFilePath(path)); } catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex)) { @@ -976,7 +916,7 @@ internal static void DeleteDirectoryNoThrow(string path, bool recursive, int ret retryCount = retryCount < 1 ? 2 : retryCount; retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut; - path = FixFilePath(path); + path = FrameworkFileUtilities.FixFilePath(path); for (int i = 0; i < retryCount; i++) { @@ -1015,7 +955,7 @@ internal static void DeleteWithoutTrailingBackslash(string path, bool recursive { try { - Directory.Delete(EnsureNoTrailingSlash(path), recursive); + Directory.Delete(FrameworkFileUtilities.EnsureNoTrailingSlash(path), recursive); // If we got here, the directory was successfully deleted return; @@ -1243,7 +1183,7 @@ internal static string MakeRelative(string basePath, string path) if (path.IndexOf(splitPath[0]) != indexOfFirstNonSlashChar) { // path was already relative so just return it - return FixFilePath(path); + return FrameworkFileUtilities.FixFilePath(path); } int index = 0; @@ -1294,7 +1234,7 @@ internal static string AttemptToShortenPath(string path) // Attempt to make it shorter -- perhaps there are some \..\ elements path = GetFullPathNoThrow(path); } - return FixFilePath(path); + return FrameworkFileUtilities.FixFilePath(path); } public static bool IsPathTooLong(string path) @@ -1318,7 +1258,7 @@ private static bool IsRootedNoThrow(string path) { try { - return Path.IsPathRooted(FixFilePath(path)); + return Path.IsPathRooted(FrameworkFileUtilities.FixFilePath(path)); } catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex)) { @@ -1369,7 +1309,7 @@ internal static string CombinePaths(string root, params string[] paths) internal static string TrimTrailingSlashes(this string s) { - return s.TrimEnd(Slashes); + return s.TrimEnd(FrameworkFileUtilities.Slashes); } /// @@ -1399,7 +1339,7 @@ internal static string ToPlatformSlash(this string s) internal static string WithTrailingSlash(this string s) { - return EnsureTrailingSlash(s); + return FrameworkFileUtilities.EnsureTrailingSlash(s); } internal static string NormalizeForPathComparison(this string s) => s.ToPlatformSlash().TrimTrailingSlashes(); diff --git a/src/Shared/FrameworkLocationHelper.cs b/src/Shared/FrameworkLocationHelper.cs index 951dcbb70d7..a7e5d74b727 100644 --- a/src/Shared/FrameworkLocationHelper.cs +++ b/src/Shared/FrameworkLocationHelper.cs @@ -7,6 +7,8 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; +using Microsoft.Build.Framework; + #if FEATURE_WIN32_REGISTRY using Microsoft.Win32; #endif @@ -1572,7 +1574,7 @@ public virtual string GetPathToDotNetFrameworkReferenceAssemblies() string referencePath = GenerateReferenceAssemblyPath(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, this.FrameworkName); if (FileSystems.Default.DirectoryExists(referencePath)) { - this._pathToDotNetFrameworkReferenceAssemblies = FileUtilities.EnsureTrailingSlash(referencePath); + this._pathToDotNetFrameworkReferenceAssemblies = FrameworkFileUtilities.EnsureTrailingSlash(referencePath); } } diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index 4aa870d7a09..f455dc9aae5 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.IO; using Microsoft.Build.Internal; @@ -293,11 +294,13 @@ internal static class NodePacketTypeExtensions /// Version 1: Introduced for the .NET Task Host protocol. This version /// excludes the translation of appDomainConfig within TaskHostConfiguration /// to maintain backward compatibility and reduce serialization overhead. + /// + /// Version 2: Adds support of HostServices and target name translation in TaskHostConfiguration. /// /// When incrementing this version, ensure compatibility with existing /// task hosts and update the corresponding deserialization logic. /// - public const byte PacketVersion = 1; + public const byte PacketVersion = 2; // Flag bits in upper 2 bits private const byte ExtendedHeaderFlag = 0x40; // Bit 6: 01000000 @@ -326,7 +329,9 @@ internal static class NodePacketTypeExtensions /// True if extended header flag was set, false otherwise. public static bool TryCreateExtendedHeaderType(HandshakeOptions handshakeOptions, NodePacketType type, out byte extendedheader) { - if (Handshake.IsHandshakeOptionEnabled(handshakeOptions, Handshake.NetTaskHostFlags)) + // Extended headers are supported by all task hosts except CLR2. + if (Handshake.IsHandshakeOptionEnabled(handshakeOptions, HandshakeOptions.TaskHost) + && !Handshake.IsHandshakeOptionEnabled(handshakeOptions, HandshakeOptions.CLR2)) { extendedheader = (byte)((byte)type | ExtendedHeaderFlag); return true; @@ -361,5 +366,18 @@ public static byte ReadVersion(Stream stream) /// The stream to write the version byte to. /// The protocol version to write to the stream. public static void WriteVersion(Stream stream, byte version) => stream.WriteByte(version); + + /// + /// Negotiates the packet version to use for communication between nodes. + /// Returns the lower of the two versions to ensure compatibility between + /// nodes that may be running different versions of MSBuild. + /// + /// This allows forward and backward compatibility when nodes with different + /// packet versions communicate - they will use the lowest common version + /// that both understand. + /// + /// The packet version supported by the other node. + /// The negotiated protocol version that both nodes can use (the minimum of the two versions). + public static byte GetNegotiatedPacketVersion(byte otherPacketVersion) => Math.Min(PacketVersion, otherPacketVersion); } } diff --git a/src/Shared/Modifiers.cs b/src/Shared/Modifiers.cs index 44786c43e21..5e17fc8b41d 100644 --- a/src/Shared/Modifiers.cs +++ b/src/Shared/Modifiers.cs @@ -9,6 +9,7 @@ #endif using System.Diagnostics.CodeAnalysis; using System.IO; +using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; #nullable disable @@ -213,7 +214,7 @@ internal static string GetItemSpecModifier(string currentDirectory, string itemS modifiedItemSpec = Path.GetPathRoot(fullPath); - if (!EndsWithSlash(modifiedItemSpec)) + if (!FrameworkFileUtilities.EndsWithSlash(modifiedItemSpec)) { ErrorUtilities.VerifyThrow(FileUtilitiesRegex.StartsWithUncPattern(modifiedItemSpec), "Only UNC shares should be missing trailing slashes."); @@ -235,7 +236,7 @@ internal static string GetItemSpecModifier(string currentDirectory, string itemS else { // Fix path to avoid problem with Path.GetFileNameWithoutExtension when backslashes in itemSpec on Unix - modifiedItemSpec = Path.GetFileNameWithoutExtension(FixFilePath(itemSpec)); + modifiedItemSpec = Path.GetFileNameWithoutExtension(FrameworkFileUtilities.FixFilePath(itemSpec)); } } else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.Extension, StringComparison.OrdinalIgnoreCase)) @@ -276,7 +277,7 @@ internal static string GetItemSpecModifier(string currentDirectory, string itemS if (length != -1) { - ErrorUtilities.VerifyThrow((modifiedItemSpec.Length > length) && IsSlash(modifiedItemSpec[length]), + ErrorUtilities.VerifyThrow((modifiedItemSpec.Length > length) && FrameworkFileUtilities.IsSlash(modifiedItemSpec[length]), "Root directory must have a trailing slash."); modifiedItemSpec = modifiedItemSpec.Substring(length + 1); @@ -284,7 +285,7 @@ internal static string GetItemSpecModifier(string currentDirectory, string itemS } else { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(modifiedItemSpec) && IsSlash(modifiedItemSpec[0]), + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(modifiedItemSpec) && FrameworkFileUtilities.IsSlash(modifiedItemSpec[0]), "Expected a full non-windows path rooted at '/'."); // A full unix path is always rooted at diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index a226056c575..038c7949dbb 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -121,6 +121,11 @@ internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint /// private BinaryWriter _binaryWriter; + /// + /// Represents the version of the parent packet associated with the node instantiation. + /// + private byte _parentPacketVersion; + #if NET /// /// The set of property names from handshake responsible for node version. @@ -217,9 +222,9 @@ public void ClientWillDisconnect() #region Construction /// - /// Instantiates an endpoint to act as a client + /// Instantiates an endpoint to act as a client. /// - internal void InternalConstruct(string pipeName = null) + internal void InternalConstruct(string pipeName = null, byte parentPacketVersion = 1) { _status = LinkStatus.Inactive; _asyncDataMonitor = new object(); @@ -227,6 +232,7 @@ internal void InternalConstruct(string pipeName = null) _packetStream = new MemoryStream(); _binaryWriter = new BinaryWriter(_packetStream); + _parentPacketVersion = parentPacketVersion; pipeName ??= NamedPipeUtil.GetPlatformSpecificPipeName(); @@ -452,9 +458,17 @@ private void PacketPumpProc() _pipeServer.TryReadEndOfHandshakeSignal(false, out HandshakeResult _)) #endif { + // Send supported PacketVersion after EndOfHandshakeSignal + // Based on this parent node decides how to communicate with the child. + if (_parentPacketVersion >= 2) + { + _pipeServer.WriteIntForHandshake(Handshake.PacketVersionFromChildMarker); // Marker: PacketVersion follows + _pipeServer.WriteIntForHandshake(NodePacketTypeExtensions.PacketVersion); + CommunicationsUtilities.Trace("Sent PacketVersion: {0}", NodePacketTypeExtensions.PacketVersion); + } + CommunicationsUtilities.Trace("Successfully connected to parent."); _pipeServer.WriteEndOfHandshakeSignal(); - #if FEATURE_SECURITY_PERMISSIONS // We will only talk to a host that was started by the same user as us. Even though the pipe access is set to only allow this user, we want to ensure they // haven't attempted to change those permissions out from under us. This ensures that the only way they can truly gain access is to be impersonating the @@ -696,16 +710,18 @@ private void RunReadLoop( bool hasExtendedHeader = NodePacketTypeExtensions.HasExtendedHeader(rawType); NodePacketType packetType = hasExtendedHeader ? NodePacketTypeExtensions.GetNodePacketType(rawType) : (NodePacketType)rawType; - byte version = 0; + byte parentVersion = 0; if (hasExtendedHeader) { - version = NodePacketTypeExtensions.ReadVersion(localReadPipe); + parentVersion = NodePacketTypeExtensions.ReadVersion(localReadPipe); } try { ITranslator readTranslator = BinaryTranslator.GetReadTranslator(localReadPipe, _sharedReadBuffer); - readTranslator.PacketVersion = version; + + // parent sends a packet version that is already negotiated during handshake. + readTranslator.NegotiatedPacketVersion = parentVersion; _packetFactory.DeserializeAndRoutePacket(0, packetType, readTranslator); } catch (Exception e) diff --git a/src/Shared/NodePipeServer.cs b/src/Shared/NodePipeServer.cs index 05d0c62361e..e5ff7c2ae25 100644 --- a/src/Shared/NodePipeServer.cs +++ b/src/Shared/NodePipeServer.cs @@ -204,7 +204,6 @@ private bool ValidateHandshake() foreach (var component in HandshakeComponents.EnumerateComponents()) { // This will disconnect a < 16.8 host; it expects leading 00 or F5 or 06. 0x00 is a wildcard. - if ( _pipeServer.TryReadIntForHandshake(byteToAccept: index == 0 ? CommunicationsUtilities.handshakeVersion : null, diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index 5cfc3f77e6c..ddf1d1c1a5d 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Microsoft.Build.Execution; using Microsoft.Build.Shared; #nullable disable @@ -84,6 +85,20 @@ internal class TaskHostConfiguration : INodePacket /// private bool _isTaskInputLoggingEnabled; + /// + /// Target name that is requesting the task execution. + /// + private string _targetName; + + /// + /// Project file path that is requesting the task execution. + /// + private string _projectFile; + +#if !NET35 + private HostServices _hostServices; +#endif + /// /// The set of parameters to apply to the task prior to execution. /// @@ -98,47 +113,53 @@ internal class TaskHostConfiguration : INodePacket #if FEATURE_APPDOMAIN /// - /// Constructor. + /// Initializes a new instance of the class. /// /// The ID of the node being configured. /// The startup directory for the task being executed. /// The set of environment variables to apply to the task execution process. /// The culture of the thread that will execute the task. /// The UI culture of the thread that will execute the task. + /// The host services to be used by the task host. /// The AppDomainSetup that may be used to pass information to an AppDomainIsolated task. /// The line number of the location from which this task was invoked. /// The column number of the location from which this task was invoked. /// The project file from which this task was invoked. - /// Flag to continue with the build after a the task failed - /// Name of the task. - /// Location of the assembly the task is to be loaded from. - /// Whether task inputs are logged. - /// Parameters to apply to the task. - /// global properties for the current project. - /// Warning codes to be treated as errors for the current project. - /// Warning codes not to be treated as errors for the current project. - /// Warning codes to be treated as messages for the current project. + /// A flag to indicate whether to continue with the build after the task fails. + /// The name of the task. + /// The location of the assembly from which the task is to be loaded. + /// The name of the target that is requesting the task execution. + /// The project path that invokes the task. + /// A flag to indicate whether task inputs are logged. + /// The parameters to apply to the task. + /// The global properties for the current project. + /// A collection of warning codes to be treated as errors. + /// A collection of warning codes not to be treated as errors. + /// A collection of warning codes to be treated as messages. #else /// - /// Constructor. + /// Initializes a new instance of the class. /// /// The ID of the node being configured. /// The startup directory for the task being executed. /// The set of environment variables to apply to the task execution process. /// The culture of the thread that will execute the task. /// The UI culture of the thread that will execute the task. + /// The host services to be used by the task host. /// The line number of the location from which this task was invoked. /// The column number of the location from which this task was invoked. /// The project file from which this task was invoked. - /// Flag to continue with the build after a the task failed - /// Name of the task. - /// Location of the assembly the task is to be loaded from. - /// Whether task inputs are logged. - /// Parameters to apply to the task. - /// global properties for the current project. - /// Warning codes to be logged as errors for the current project. - /// Warning codes not to be treated as errors for the current project. - /// Warning codes to be treated as messages for the current project. + /// A flag to indicate whether to continue with the build after the task fails. + /// The name of the task. + /// The location of the assembly from which the task is to be loaded. + /// The name of the target that is requesting the task execution. + /// The project path that invokes the task. + /// A flag to indicate whether task inputs are logged. + /// The parameters to apply to the task. + /// The global properties for the current project. + /// A collection of warning codes to be treated as errors. + /// A collection of warning codes not to be treated as errors. + /// A collection of warning codes to be treated as messages. #endif public TaskHostConfiguration( int nodeId, @@ -146,6 +167,9 @@ public TaskHostConfiguration( IDictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, +#if !NET35 + HostServices hostServices, +#endif #if FEATURE_APPDOMAIN AppDomainSetup appDomainSetup, #endif @@ -155,6 +179,8 @@ public TaskHostConfiguration( bool continueOnError, string taskName, string taskLocation, + string targetName, + string projectFile, bool isTaskInputLoggingEnabled, IDictionary taskParameters, Dictionary globalParameters, @@ -180,15 +206,20 @@ public TaskHostConfiguration( _culture = culture; _uiCulture = uiCulture; +#if !NET35 + _hostServices = hostServices; +#endif #if FEATURE_APPDOMAIN _appDomainSetup = appDomainSetup; #endif _lineNumberOfTask = lineNumberOfTask; _columnNumberOfTask = columnNumberOfTask; _projectFileOfTask = projectFileOfTask; + _projectFile = projectFile; _continueOnError = continueOnError; _taskName = taskName; _taskLocation = taskLocation; + _targetName = targetName; _isTaskInputLoggingEnabled = isTaskInputLoggingEnabled; _warningsAsErrors = warningsAsErrors; _warningsNotAsErrors = warningsNotAsErrors; @@ -277,6 +308,18 @@ public AppDomainSetup AppDomainSetup } #endif +#if !NET35 + /// + /// The HostServices to be used by the task host. + /// + public HostServices HostServices + { + [DebuggerStepThrough] + get + { return _hostServices; } + } +#endif + /// /// Line number where the instance of this task is defined. /// @@ -297,6 +340,26 @@ public int ColumnNumberOfTask { return _columnNumberOfTask; } } + /// + /// Project file path that is requesting the task execution. + /// + public string ProjectFile + { + [DebuggerStepThrough] + get + { return _projectFile; } + } + + /// + /// Target name that is requesting the task execution. + /// + public string TargetName + { + [DebuggerStepThrough] + get + { return _targetName; } + } + /// /// ContinueOnError flag for this particular task /// @@ -420,7 +483,7 @@ public void Translate(ITranslator translator) // If the packet version is bigger then 0, it means the task host will running under .NET. // Although MSBuild.exe runs under .NET Framework and has AppDomain support, // we don't transmit AppDomain config when communicating with dotnet.exe (it is not supported in .NET 5+). - if (translator.PacketVersion == 0) + if (translator.NegotiatedPacketVersion == 0) { byte[] appDomainConfigBytes = null; @@ -444,6 +507,15 @@ public void Translate(ITranslator translator) translator.Translate(ref _projectFileOfTask); translator.Translate(ref _taskName); translator.Translate(ref _taskLocation); + if (translator.NegotiatedPacketVersion >= 2) + { + translator.Translate(ref _targetName); + translator.Translate(ref _projectFile); +#if !NET35 + translator.Translate(ref _hostServices); +#endif + } + translator.Translate(ref _isTaskInputLoggingEnabled); translator.TranslateDictionary(ref _taskParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); translator.Translate(ref _continueOnError); diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs index 1f288534e81..fcfa634e4c4 100644 --- a/src/Shared/TaskLoggingHelper.cs +++ b/src/Shared/TaskLoggingHelper.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.Globalization; using System.IO; using System.Resources; diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs index 909b9863dc1..3533bf22763 100644 --- a/src/Shared/TempFileUtilities.cs +++ b/src/Shared/TempFileUtilities.cs @@ -5,6 +5,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Threading; +using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; #nullable disable @@ -70,7 +71,7 @@ private static string CreateFolderUnderTemp() Directory.CreateDirectory(path); #endif - return FileUtilities.EnsureTrailingSlash(path); + return FrameworkFileUtilities.EnsureTrailingSlash(path); } /// diff --git a/src/Shared/UnitTests/FileMatcher_Tests.cs b/src/Shared/UnitTests/FileMatcher_Tests.cs index bf7f53b6bf3..dc90922a428 100644 --- a/src/Shared/UnitTests/FileMatcher_Tests.cs +++ b/src/Shared/UnitTests/FileMatcher_Tests.cs @@ -95,7 +95,7 @@ public void DoNotFollowRecursiveSymlinks() #endif [Theory] - [MemberData(nameof(GetFilesComplexGlobbingMatchingInfo.GetTestData), MemberType = typeof(GetFilesComplexGlobbingMatchingInfo))] + [MemberData(nameof(GetFilesComplexGlobbingMatchingInfo.GetTestData), MemberType = typeof(GetFilesComplexGlobbingMatchingInfo), DisableDiscoveryEnumeration = true)] public void GetFilesComplexGlobbingMatching(GetFilesComplexGlobbingMatchingInfo info) { TransientTestFolder testFolder = _env.CreateFolder(); @@ -1873,8 +1873,8 @@ public void GetFileSpecInfoCommon( { if (NativeMethodsShared.IsUnixLike) { - expectedFixedDirectoryPart = FileUtilities.FixFilePath(expectedFixedDirectoryPart); - expectedWildcardDirectoryPart = FileUtilities.FixFilePath(expectedWildcardDirectoryPart); + expectedFixedDirectoryPart = FrameworkFileUtilities.FixFilePath(expectedFixedDirectoryPart); + expectedWildcardDirectoryPart = FrameworkFileUtilities.FixFilePath(expectedWildcardDirectoryPart); } TestGetFileSpecInfo( filespec, @@ -2299,11 +2299,11 @@ private bool IsMatchingDirectory(string path, string candidate) { if (String.Compare(normalizedPath, 0, normalizedCandidate, 0, normalizedPath.Length, StringComparison.OrdinalIgnoreCase) == 0) { - if (FileUtilities.EndsWithSlash(normalizedPath)) + if (FrameworkFileUtilities.EndsWithSlash(normalizedPath)) { return true; } - else if (FileUtilities.IsSlash(normalizedCandidate[normalizedPath.Length])) + else if (FrameworkFileUtilities.IsSlash(normalizedCandidate[normalizedPath.Length])) { return true; } @@ -2508,9 +2508,9 @@ private static void ValidateSplitFileSpec( out wildcardDirectoryPart, out filenamePart); - expectedFixedDirectoryPart = FileUtilities.FixFilePath(expectedFixedDirectoryPart); - expectedWildcardDirectoryPart = FileUtilities.FixFilePath(expectedWildcardDirectoryPart); - expectedFilenamePart = FileUtilities.FixFilePath(expectedFilenamePart); + expectedFixedDirectoryPart = FrameworkFileUtilities.FixFilePath(expectedFixedDirectoryPart); + expectedWildcardDirectoryPart = FrameworkFileUtilities.FixFilePath(expectedWildcardDirectoryPart); + expectedFilenamePart = FrameworkFileUtilities.FixFilePath(expectedFilenamePart); if ( diff --git a/src/Shared/UnitTests/FileUtilities_Tests.cs b/src/Shared/UnitTests/FileUtilities_Tests.cs index 92d25812ac4..b7e677e2e4a 100644 --- a/src/Shared/UnitTests/FileUtilities_Tests.cs +++ b/src/Shared/UnitTests/FileUtilities_Tests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Shouldly; using Xunit; @@ -211,27 +212,27 @@ public void GetFileInfoNoThrowNonexistent() } /// - /// Exercises FileUtilities.EndsWithSlash + /// Exercises FrameworkFileUtilities.EndsWithSlash /// [Fact] [Trait("Category", "netcore-osx-failing")] [Trait("Category", "netcore-linux-failing")] public void EndsWithSlash() { - Assert.True(FileUtilities.EndsWithSlash(@"C:\foo\")); - Assert.True(FileUtilities.EndsWithSlash(@"C:\")); - Assert.True(FileUtilities.EndsWithSlash(@"\")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"C:\foo\")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"C:\")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"\")); - Assert.True(FileUtilities.EndsWithSlash(@"http://www.microsoft.com/")); - Assert.True(FileUtilities.EndsWithSlash(@"//server/share/")); - Assert.True(FileUtilities.EndsWithSlash(@"/")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"http://www.microsoft.com/")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"//server/share/")); + Assert.True(FrameworkFileUtilities.EndsWithSlash(@"/")); - Assert.False(FileUtilities.EndsWithSlash(@"C:\foo")); - Assert.False(FileUtilities.EndsWithSlash(@"C:")); - Assert.False(FileUtilities.EndsWithSlash(@"foo")); + Assert.False(FrameworkFileUtilities.EndsWithSlash(@"C:\foo")); + Assert.False(FrameworkFileUtilities.EndsWithSlash(@"C:")); + Assert.False(FrameworkFileUtilities.EndsWithSlash(@"foo")); // confirm that empty string doesn't barf - Assert.False(FileUtilities.EndsWithSlash(String.Empty)); + Assert.False(FrameworkFileUtilities.EndsWithSlash(String.Empty)); } /// @@ -245,16 +246,16 @@ public void GetDirectoryWithTrailingSlash() Assert.Equal(NativeMethodsShared.IsWindows ? @"c:\" : "/", FileUtilities.GetDirectory(NativeMethodsShared.IsWindows ? @"c:\" : "/")); Assert.Equal(NativeMethodsShared.IsWindows ? @"c:\" : "/", FileUtilities.GetDirectory(NativeMethodsShared.IsWindows ? @"c:\foo" : "/foo")); Assert.Equal(NativeMethodsShared.IsWindows ? @"c:" : "/", FileUtilities.GetDirectory(NativeMethodsShared.IsWindows ? @"c:" : "/")); - Assert.Equal(FileUtilities.FixFilePath(@"\"), FileUtilities.GetDirectory(@"\")); - Assert.Equal(FileUtilities.FixFilePath(@"\"), FileUtilities.GetDirectory(@"\foo")); - Assert.Equal(FileUtilities.FixFilePath(@"..\"), FileUtilities.GetDirectory(@"..\foo")); - Assert.Equal(FileUtilities.FixFilePath(@"\foo\"), FileUtilities.GetDirectory(@"\foo\")); - Assert.Equal(FileUtilities.FixFilePath(@"\\server\share"), FileUtilities.GetDirectory(@"\\server\share")); - Assert.Equal(FileUtilities.FixFilePath(@"\\server\share\"), FileUtilities.GetDirectory(@"\\server\share\")); - Assert.Equal(FileUtilities.FixFilePath(@"\\server\share\"), FileUtilities.GetDirectory(@"\\server\share\file")); - Assert.Equal(FileUtilities.FixFilePath(@"\\server\share\directory\"), FileUtilities.GetDirectory(@"\\server\share\directory\")); - Assert.Equal(FileUtilities.FixFilePath(@"foo\"), FileUtilities.GetDirectory(@"foo\bar")); - Assert.Equal(FileUtilities.FixFilePath(@"\foo\bar\"), FileUtilities.GetDirectory(@"\foo\bar\")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\"), FileUtilities.GetDirectory(@"\")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\"), FileUtilities.GetDirectory(@"\foo")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"..\"), FileUtilities.GetDirectory(@"..\foo")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\foo\"), FileUtilities.GetDirectory(@"\foo\")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\\server\share"), FileUtilities.GetDirectory(@"\\server\share")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\\server\share\"), FileUtilities.GetDirectory(@"\\server\share\")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\\server\share\"), FileUtilities.GetDirectory(@"\\server\share\file")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\\server\share\directory\"), FileUtilities.GetDirectory(@"\\server\share\directory\")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\"), FileUtilities.GetDirectory(@"foo\bar")); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"\foo\bar\"), FileUtilities.GetDirectory(@"\foo\bar\")); Assert.Equal(String.Empty, FileUtilities.GetDirectory("foo")); } @@ -315,20 +316,20 @@ public void HasExtension_UsesOrdinalIgnoreCase() } /// - /// Exercises FileUtilities.EnsureTrailingSlash + /// Exercises FrameworkFileUtilities.EnsureTrailingSlash /// [Fact] public void EnsureTrailingSlash() { // Doesn't have a trailing slash to start with. - Assert.Equal(FileUtilities.FixFilePath(@"foo\bar\"), FileUtilities.EnsureTrailingSlash(@"foo\bar")); // "test 1" - Assert.Equal(FileUtilities.FixFilePath(@"foo/bar\"), FileUtilities.EnsureTrailingSlash(@"foo/bar")); // "test 2" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\bar\"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo\bar")); // "test 1" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo/bar\"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo/bar")); // "test 2" // Already has a trailing slash to start with. - Assert.Equal(FileUtilities.FixFilePath(@"foo/bar/"), FileUtilities.EnsureTrailingSlash(@"foo/bar/")); // test 3" - Assert.Equal(FileUtilities.FixFilePath(@"foo\bar\"), FileUtilities.EnsureTrailingSlash(@"foo\bar\")); // test 4" - Assert.Equal(FileUtilities.FixFilePath(@"foo/bar\"), FileUtilities.EnsureTrailingSlash(@"foo/bar\")); // test 5" - Assert.Equal(FileUtilities.FixFilePath(@"foo\bar/"), FileUtilities.EnsureTrailingSlash(@"foo\bar/")); // "test 5" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo/bar/"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo/bar/")); // test 3" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\bar\"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo\bar\")); // test 4" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo/bar\"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo/bar\")); // test 5" + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\bar/"), FrameworkFileUtilities.EnsureTrailingSlash(@"foo\bar/")); // "test 5" } /// diff --git a/src/StringTools.UnitTests/InterningTestData.cs b/src/StringTools.UnitTests/InterningTestData.cs index 0e34b3ca39a..28fc95ac0e2 100644 --- a/src/StringTools.UnitTests/InterningTestData.cs +++ b/src/StringTools.UnitTests/InterningTestData.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; +using Xunit.Abstractions; + #nullable disable namespace Microsoft.NET.StringTools.Tests @@ -12,13 +14,18 @@ public static class InterningTestData /// /// Represents an array of string fragments to initialize an InternableString with. /// - public class TestDatum + public class TestDatum : IXunitSerializable { private string _string; - public string[] Fragments { get; } + public string[] Fragments { get; private set; } public int Length => _string.Length; + // Required for deserialization + public TestDatum() + { + } + public TestDatum(params string[] fragments) { Fragments = fragments; @@ -31,6 +38,17 @@ public override string ToString() { return _string; } + + public void Deserialize(IXunitSerializationInfo info) + { + Fragments = info.GetValue(nameof(Fragments)); + _string = string.Join(string.Empty, Fragments); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(Fragments), Fragments); + } } public static IEnumerable TestData diff --git a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs index 2ab31ff10f8..3b9b1740d64 100644 --- a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs +++ b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs @@ -1206,15 +1206,15 @@ public void BuildTaskSimpleCodeFactoryTempDirectoryDoesntExist() { // Ensure we're getting the right temp path (%TMP% == GetTempPath()) Assert.Equal( - FileUtilities.EnsureTrailingSlash(Path.GetTempPath()), - FileUtilities.EnsureTrailingSlash(Path.GetFullPath(oldTempPath))); + FrameworkFileUtilities.EnsureTrailingSlash(Path.GetTempPath()), + FrameworkFileUtilities.EnsureTrailingSlash(Path.GetFullPath(oldTempPath))); Assert.False(Directory.Exists(newTempPath)); Environment.SetEnvironmentVariable("TMP", newTempPath); Assert.Equal( - FileUtilities.EnsureTrailingSlash(newTempPath), - FileUtilities.EnsureTrailingSlash(Path.GetTempPath())); + FrameworkFileUtilities.EnsureTrailingSlash(newTempPath), + FrameworkFileUtilities.EnsureTrailingSlash(Path.GetTempPath())); MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); mockLogger.AssertLogContains("Hello, World!"); diff --git a/src/Tasks.UnitTests/Copy_Tests.cs b/src/Tasks.UnitTests/Copy_Tests.cs index c73787e160a..9b3bf954125 100644 --- a/src/Tasks.UnitTests/Copy_Tests.cs +++ b/src/Tasks.UnitTests/Copy_Tests.cs @@ -125,7 +125,11 @@ public void Dispose() [Fact] public void CopyWithNoInput() { - var task = new Copy { BuildEngine = new MockEngine(true), }; + var task = new Copy + { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = new MockEngine(true), + }; task.Execute().ShouldBeTrue(); (task.CopiedFiles == null || task.CopiedFiles.Length == 0).ShouldBeTrue(); (task.DestinationFiles == null || task.DestinationFiles.Length == 0).ShouldBeTrue(); @@ -141,6 +145,7 @@ public void CopyWithMatchingSourceFilesToDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -166,6 +171,7 @@ public void CopyWithSourceFilesToDestinationFolder(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -209,6 +215,7 @@ public void CopyWithSourceFoldersToDestinationFolder(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFolders = new ITaskItem[] { new TaskItem(s0Folder.Path), new TaskItem(s1Folder.Path) }, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -235,6 +242,7 @@ public void CopyWithNoSource() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, DestinationFolder = new TaskItem(destinationFolder.Path), }; @@ -264,6 +272,7 @@ public void CopyWithMultipleSourceTypes(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, SourceFolders = new ITaskItem[] { new TaskItem(sourceFolder.Path) }, @@ -289,6 +298,7 @@ public void CopyWithEmptySourceFiles(ITaskItem[] sourceFiles) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = sourceFiles, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -313,6 +323,7 @@ public void CopyWithEmptySourceFolders(ITaskItem[] sourceFolders) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFolders = sourceFolders, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -337,6 +348,7 @@ public void CopyWithNoDestination(ITaskItem[] destinationFiles) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = destinationFiles, @@ -361,6 +373,7 @@ public void CopyWithMultipleDestinationTypes() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -385,6 +398,7 @@ public void CopyWithSourceFoldersAndDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, SourceFolders = new ITaskItem[] { new TaskItem(sourceFolder.Path) }, @@ -408,6 +422,7 @@ public void CopyWithDifferentLengthSourceFilesToDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination0.txt"), new TaskItem("destination1.txt") }, @@ -433,6 +448,7 @@ public void CopyWithInvalidRetryCount() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -459,6 +475,7 @@ public void CopyWithInvalidRetryDelay() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -495,6 +512,7 @@ public void DontCopyOverSameFile(bool isUseHardLinks, bool isUseSymbolicLinks, b CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -543,6 +561,7 @@ public void QuestionCopyFile(bool isUseHardLinks, bool isUseSymbolicLinks, bool CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -603,6 +622,7 @@ public void QuestionCopyFileSameContent(bool isUseHardLinks, bool isUseSymbolicL CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -655,6 +675,7 @@ public void QuestionCopyFileNotSameContent(bool isUseHardLinks, bool isUseSymbol CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -711,6 +732,7 @@ public void DoNotNormallyCopyOverReadOnlyFile(bool isUseHardLinks, bool isUseSym var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -778,6 +800,7 @@ public void CopyOverReadOnlyFileEnvironmentOverride(bool isUseHardLinks, bool is var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -845,6 +868,7 @@ public void AlwaysRetryCopyEnvironmentOverride(bool isUseHardLinks, bool isUseSy var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -912,6 +936,7 @@ public void CopyOverReadOnlyFileParameterIsSet(bool isUseHardLinks, bool isUseSy var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -985,6 +1010,7 @@ public void CopyOverReadOnlyFileParameterIsSetWithDestinationFolder(bool isUseHa var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1051,6 +1077,7 @@ public void DoCopyOverDifferentFile(bool isUseHardLinks, bool isUseSymbolicLinks var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1107,7 +1134,8 @@ public void DoCopyOverCopiedFile(bool skipUnchangedFiles, bool isUseHardLinks, b var engine = new MockEngine(_testOutputHelper); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new[] { new TaskItem(sourceFile) }, DestinationFiles = new[] { new TaskItem(destinationFile) }, @@ -1186,6 +1214,7 @@ public void DoCopyOverNonExistentFile(bool isUseHardLinks, bool isUseSymbolicLin var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1225,6 +1254,7 @@ public void DoNotRetryCopyNotSupportedException(bool isUseHardLinks, bool isUseS var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1272,6 +1302,7 @@ public void DoNotRetryCopyNonExistentSourceFile(bool isUseHardLinks, bool isUseS var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1319,6 +1350,7 @@ public void DoNotRetryCopyWhenSourceIsFolder(bool isUseHardLinks, bool isUseSymb var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1362,6 +1394,7 @@ public void DoRetryWhenDestinationLocked(bool isUseHardLinks, bool isUseSymbolic var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1433,6 +1466,7 @@ public void DoNotRetryWhenDestinationLockedDueToAcl(bool isUseHardLinks, bool is var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }, @@ -1488,6 +1522,7 @@ public void DoNotRetryCopyWhenDestinationFolderIsFile(bool isUseHardLinks, bool var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1535,6 +1570,7 @@ public void DoNotRetryCopyWhenDestinationFileIsFolder(bool isUseHardLinks, bool var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1590,6 +1626,7 @@ public void OutputsOnlyIncludeSuccessfulCopies(bool isUseHardLinks, bool isUseSy var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, UseHardlinksIfPossible = isUseHardLinks, @@ -1672,6 +1709,7 @@ public void CopyFileOnItself(bool isUseHardLinks, bool isUseSymbolicLinks) var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1692,6 +1730,7 @@ public void CopyFileOnItself(bool isUseHardLinks, bool isUseSymbolicLinks) engine = new MockEngine(_testOutputHelper); t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, DestinationFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1743,6 +1782,7 @@ public void CopyFileOnItself2(bool isUseHardLinks, bool isUseSymbolicLinks) var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1797,6 +1837,7 @@ public void CopyFileOnItselfAndFailACopy(bool isUseHardLinks, bool isUseSymbolic var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(invalidFile) }, @@ -1858,6 +1899,7 @@ public void CopyToDestinationFolder(bool isUseHardLinks, bool isUseSymbolicLinks var me = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -1930,6 +1972,7 @@ public void CopyDoubleEscapableFileToDestinationFolder(bool isUseHardLinks, bool var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1995,7 +2038,8 @@ public void CopyWithDuplicatesUsingFolder(bool isUseHardLinks, bool isUseSymboli var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, DestinationFolder = new TaskItem(Path.Combine(tempPath, "foo")), @@ -2017,7 +2061,7 @@ public void CopyWithDuplicatesUsingFolder(bool isUseHardLinks, bool isUseSymboli Assert.Equal(4, t.CopiedFiles.Length); // Copy calls to different destinations can come in any order when running in parallel. - filesActuallyCopied.Select(f => Path.GetFileName(f.Key.Name)).ShouldBe(new[] { "a.cs", "b.cs" }, ignoreOrder: true); + filesActuallyCopied.Select(f => Path.GetFileName(f.Key.Path)).ShouldBe(new[] { "a.cs", "b.cs" }, ignoreOrder: true); ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries } @@ -2062,6 +2106,7 @@ public void CopyWithDuplicatesUsingFiles(bool isUseHardLinks, bool isUseSymbolic var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2085,16 +2130,16 @@ public void CopyWithDuplicatesUsingFiles(bool isUseHardLinks, bool isUseSymbolic // Copy calls to different destinations can come in any order when running in parallel. string xaPath = Path.Combine(tempPath, "xa.cs"); - var xaCopies = filesActuallyCopied.Where(f => f.Value.Name == xaPath).ToList(); + var xaCopies = filesActuallyCopied.Where(f => f.Value.Path == xaPath).ToList(); Assert.Equal(3, xaCopies.Count); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[0].Key.Name); - Assert.Equal(Path.Combine(tempPath, "b.cs"), xaCopies[1].Key.Name); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[2].Key.Name); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[0].Key.Path); + Assert.Equal(Path.Combine(tempPath, "b.cs"), xaCopies[1].Key.Path); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[2].Key.Path); string xbPath = Path.Combine(tempPath, "xb.cs"); - var xbCopies = filesActuallyCopied.Where(f => f.Value.Name == xbPath).ToList(); + var xbCopies = filesActuallyCopied.Where(f => f.Value.Path == xbPath).ToList(); Assert.Single(xbCopies); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xbCopies[0].Key.Name); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xbCopies[0].Key.Path); ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries } @@ -2131,6 +2176,7 @@ public void DestinationFilesLengthNotEqualSourceFilesLength(bool isUseHardLinks, var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(inFile1), new TaskItem(inFile2) }, @@ -2179,6 +2225,7 @@ public void Regress451057_ExitGracefullyIfPathNameIsTooLong(bool isUseHardLinks, var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2217,6 +2264,7 @@ public void Regress451057_ExitGracefullyIfPathNameIsTooLong2(bool isUseHardLinks var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2243,7 +2291,8 @@ public void ExitGracefullyOnInvalidPathCharacters(bool isUseHardLinks, bool isUs { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = new ITaskItem[] { new TaskItem("foo | bar") }, DestinationFolder = new TaskItem("dest"), @@ -2267,7 +2316,8 @@ public void ExitGracefullyOnInvalidPathCharactersInDestinationFolder(bool isUseH { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = new ITaskItem[] { new TaskItem("foo") }, DestinationFolder = new TaskItem("here | there"), @@ -2291,7 +2341,8 @@ public void InvalidRetryCount() var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2313,7 +2364,8 @@ public void InvalidRetryDelayCount() var engine = new MockEngine(true /* log to console */); var t = new Copy { - BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, Retries = 1, @@ -2337,7 +2389,8 @@ public void FailureWithNoRetries(bool isUseHardLinks, bool isUseSymbolicLinks, b var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2362,6 +2415,7 @@ public void DefaultRetriesIs10() { var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! }; @@ -2387,7 +2441,8 @@ public void DefaultNoHardlink() { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! }; Assert.False(t.UseHardlinksIfPossible); @@ -2404,7 +2459,8 @@ public void SuccessAfterOneRetry(bool isUseHardLinks, bool isUseSymbolicLinks, b var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 0, // Can't really test the delay, but at least try passing in a value + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 0, // Can't really test the delay, but at least try passing in a value BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2431,7 +2487,8 @@ public void SuccessAfterOneRetryContinueToNextFile(bool isUseHardLinks, bool isU var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // Can't really test the delay, but at least try passing in a value + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // Can't really test the delay, but at least try passing in a value BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source"), new TaskItem("c:\\source2") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination"), new TaskItem("c:\\destination2") }, @@ -2448,8 +2505,10 @@ public void SuccessAfterOneRetryContinueToNextFile(bool isUseHardLinks, bool isU engine.AssertLogDoesntContain("MSB3027"); // Copy calls to different destinations can come in any order when running in parallel. - Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Name == FileUtilities.FixFilePath("c:\\source")); - Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Name == FileUtilities.FixFilePath("c:\\source2")); + // Use .OriginalValue to compare against the original input path (before Path.GetFullPath resolution). + // TaskItem normalizes paths via FileUtilities.FixFilePath, so we need to do the same for comparison. + Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Path.OriginalValue == FrameworkFileUtilities.FixFilePath("c:\\source")); + Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Path.OriginalValue == FrameworkFileUtilities.FixFilePath("c:\\source2")); } /// @@ -2463,6 +2522,7 @@ public void TooFewRetriesReturnsFalse(bool isUseHardLinks, bool isUseSymbolicLin var engine = new MockEngine(true /* log to console */); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, @@ -2492,6 +2552,7 @@ public void TooFewRetriesThrows(bool isUseHardLinks, bool isUseSymbolicLinks, bo var engine = new MockEngine(true /* log to console */); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, @@ -2524,6 +2585,7 @@ public void ErrorIfLinkFailedCheck(bool isUseHardLinks, bool isUseSymbolicLinks) MockEngine engine = new MockEngine(_testOutputHelper); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, UseHardlinksIfPossible = isUseHardLinks, UseSymboliclinksIfPossible = isUseSymbolicLinks, @@ -2559,6 +2621,7 @@ public void CopyToDestinationFolderWithHardLinkCheck() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2649,6 +2712,7 @@ public void CopyToDestinationFolderWithHardLinkFallbackNetwork() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, BuildEngine = me, @@ -2735,6 +2799,7 @@ public void CopyToDestinationFolderWithHardLinkFallbackTooManyLinks() MockEngine me = new MockEngine(true); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, BuildEngine = me, @@ -2814,6 +2879,7 @@ public void CopyToDestinationFolderWithSymbolicLinkCheck() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2878,6 +2944,7 @@ public void CopyWithHardAndSymbolicLinks() MockEngine me = new MockEngine(true); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, UseSymboliclinksIfPossible = true, @@ -2909,7 +2976,8 @@ public void InvalidErrorIfLinkFailed() var engine = new MockEngine(true); var t = new Copy { - BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, UseHardlinksIfPossible = false, @@ -2946,6 +3014,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2963,6 +3032,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2986,6 +3056,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -3124,6 +3195,7 @@ public void CopyToFileWithSameCaseInsensitiveNameAsExistingDirectoryOnUnix() string destFile2 = Path.Combine(outputDir, "app.dll"); Copy t = new Copy(); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; t.SourceFiles = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs b/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs index a29f5b938fc..c0b37c2f639 100644 --- a/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs +++ b/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs @@ -326,7 +326,7 @@ public void CulturedBitmapWithRootNamespace() binaryStream: null, log: null); - Assert.Equal(FileUtilities.FixFilePath(@"fr\RootNamespace.SubFolder.SplashScreen.bmp"), result); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"fr\RootNamespace.SubFolder.SplashScreen.bmp"), result); } /// diff --git a/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs b/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs index 567eea82ab6..d7ce809ecfa 100644 --- a/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs +++ b/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs @@ -217,7 +217,7 @@ public void RootnamespaceWithCulture() { string result = CreateVisualBasicManifestResourceName.CreateManifestNameImpl( - fileName: FileUtilities.FixFilePath(@"SubFolder\MyForm.en-GB.ResX"), + fileName: FrameworkFileUtilities.FixFilePath(@"SubFolder\MyForm.en-GB.ResX"), linkFileName: null, // Link file name prependCultureAsDirectory: @@ -283,7 +283,7 @@ public void BitmapWithRootNamespace() { string result = CreateVisualBasicManifestResourceName.CreateManifestNameImpl( - fileName: FileUtilities.FixFilePath(@"SubFolder\SplashScreen.bmp"), + fileName: FrameworkFileUtilities.FixFilePath(@"SubFolder\SplashScreen.bmp"), linkFileName: null, // Link file name prependCultureAsDirectory: true, rootNamespace: "RootNamespace", // Root namespace @@ -303,7 +303,7 @@ public void CulturedBitmapWithRootNamespace() { string result = CreateVisualBasicManifestResourceName.CreateManifestNameImpl( - fileName: FileUtilities.FixFilePath(@"SubFolder\SplashScreen.fr.bmp"), + fileName: FrameworkFileUtilities.FixFilePath(@"SubFolder\SplashScreen.fr.bmp"), linkFileName: null, // Link file name prependCultureAsDirectory: true, rootNamespace: "RootNamespace", // Root namespace @@ -312,7 +312,7 @@ public void CulturedBitmapWithRootNamespace() binaryStream: null, log: null); - Assert.Equal(FileUtilities.FixFilePath(@"fr\RootNamespace.SplashScreen.bmp"), result); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"fr\RootNamespace.SplashScreen.bmp"), result); } /// @@ -323,7 +323,7 @@ public void CulturedBitmapWithRootNamespaceNoDirectoryPrefix() { string result = CreateVisualBasicManifestResourceName.CreateManifestNameImpl( - fileName: FileUtilities.FixFilePath(@"SubFolder\SplashScreen.fr.bmp"), + fileName: FrameworkFileUtilities.FixFilePath(@"SubFolder\SplashScreen.fr.bmp"), linkFileName: null, // Link file name prependCultureAsDirectory: false, rootNamespace: "RootNamespace", // Root namespace @@ -614,7 +614,7 @@ public void CulturedResourcesFileWithRootNamespaceWithinSubfolder() { string result = CreateVisualBasicManifestResourceName.CreateManifestNameImpl( - fileName: FileUtilities.FixFilePath(@"SubFolder\MyResource.fr.resources"), + fileName: FrameworkFileUtilities.FixFilePath(@"SubFolder\MyResource.fr.resources"), linkFileName: null, // Link file name prependCultureAsDirectory: false, rootNamespace: "RootNamespace", // Root namespace diff --git a/src/Tasks.UnitTests/Delete_Tests.cs b/src/Tasks.UnitTests/Delete_Tests.cs index e523c13816c..85a3629987b 100644 --- a/src/Tasks.UnitTests/Delete_Tests.cs +++ b/src/Tasks.UnitTests/Delete_Tests.cs @@ -24,6 +24,7 @@ public sealed class Delete_Tests public void AttributeForwarding() { Delete t = new Delete(); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); ITaskItem i = new TaskItem("MyFiles.nonexistent"); i.SetMetadata("Locale", "en-GB"); @@ -59,6 +60,7 @@ public void DeleteWithRetries() var t = new Delete { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(), Files = sourceFiles, @@ -75,6 +77,7 @@ public void DeleteWithRetries() ITaskItem[] duplicateSourceFiles = { sourceItem, sourceItem }; t = new Delete { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(), Files = duplicateSourceFiles, diff --git a/src/Tasks.UnitTests/FileStateTests.cs b/src/Tasks.UnitTests/FileStateTests.cs index 1b23ffaba7d..e51abea01cf 100644 --- a/src/Tasks.UnitTests/FileStateTests.cs +++ b/src/Tasks.UnitTests/FileStateTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Tasks; using Xunit; @@ -16,30 +17,35 @@ namespace Microsoft.Build.UnitTests /// public class FileStateTests { + /// + /// Helper to create AbsolutePath for tests, bypassing rooted check for test paths. + /// + private static AbsolutePath TestPath(string path) => new AbsolutePath(path, ignoreRootedCheck: true); + [Fact] public void BadNoName() { Assert.Throws(() => { - new FileState(""); + new FileState(TestPath("")); }); } [Fact] public void BadCharsCtorOK() { - new FileState("|"); + new FileState(TestPath("|")); } [Fact] public void BadTooLongCtorOK() { - new FileState(new String('x', 5000)); + new FileState(TestPath(new String('x', 5000))); } [WindowsFullFrameworkOnlyFact(additionalMessage: ".NET Core 2.1+ no longer validates paths: https://github.com/dotnet/corefx/issues/27779#issuecomment-371253486. On Unix there is no invalid file name characters.")] public void BadChars() { - var state = new FileState("|"); + var state = new FileState(TestPath("|")); Assert.Throws(() => { var time = state.LastWriteTime; }); } @@ -48,7 +54,7 @@ public void BadTooLongLastWriteTime() { Helpers.VerifyAssertThrowsSameWay( delegate () { var x = new FileInfo(new String('x', 5000)).LastWriteTime; }, - delegate () { var x = new FileState(new String('x', 5000)).LastWriteTime; }); + delegate () { var x = new FileState(TestPath(new String('x', 5000))).LastWriteTime; }); } [Fact] @@ -60,7 +66,7 @@ public void Exists() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Exists, state.FileExists); } @@ -79,9 +85,9 @@ public void Name() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); - Assert.Equal(info.FullName, state.Name); + Assert.Equal(info.FullName, state.Path); } finally { @@ -92,7 +98,7 @@ public void Name() [Fact] public void IsDirectoryTrue() { - var state = new FileState(Path.GetTempPath()); + var state = new FileState(TestPath(Path.GetTempPath())); Assert.True(state.IsDirectory); } @@ -106,7 +112,7 @@ public void LastWriteTime() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTime, state.LastWriteTime); } @@ -125,7 +131,7 @@ public void LastWriteTimeUtc() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); } @@ -144,7 +150,7 @@ public void Length() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Length, state.Length); } @@ -163,7 +169,7 @@ public void ReadOnly() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.IsReadOnly, state.IsReadOnly); } @@ -182,7 +188,7 @@ public void ExistsReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Exists, state.FileExists); File.Delete(file); @@ -208,16 +214,16 @@ public void NameReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); - Assert.Equal(info.FullName, state.Name); + Assert.Equal(info.FullName, state.Path); string originalName = info.FullName; string oldFile = file; file = oldFile + "2"; File.Move(oldFile, file); - Assert.Equal(originalName, state.Name); + Assert.Equal(originalName, state.Path); state.Reset(); - Assert.Equal(originalName, state.Name); // Name is from the constructor, didn't change + Assert.Equal(originalName, state.Path); // Name is from the constructor, didn't change } finally { @@ -234,7 +240,7 @@ public void LastWriteTimeReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTime, state.LastWriteTime); @@ -260,7 +266,7 @@ public void LastWriteTimeUtcReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); @@ -288,7 +294,7 @@ public void LengthReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Length, state.Length); File.WriteAllText(file, "x"); @@ -313,7 +319,7 @@ public void ReadOnlyReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.IsReadOnly, state.IsReadOnly); info.IsReadOnly = !info.IsReadOnly; @@ -330,32 +336,32 @@ public void ReadOnlyReset() [Fact] public void ExistsButDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(Path.GetTempPath()).FileExists); - Assert.True(new FileState(Path.GetTempPath()).IsDirectory); + Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(TestPath(Path.GetTempPath())).FileExists); + Assert.True(new FileState(TestPath(Path.GetTempPath())).IsDirectory); } [Fact] public void ReadOnlyOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(Path.GetTempPath()).IsReadOnly); + Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(TestPath(Path.GetTempPath())).IsReadOnly); } [Fact] public void LastWriteTimeOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(Path.GetTempPath()).LastWriteTime); + Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(TestPath(Path.GetTempPath())).LastWriteTime); } [Fact] public void LastWriteTimeUtcOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(Path.GetTempPath()).LastWriteTimeUtcFast); + Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(TestPath(Path.GetTempPath())).LastWriteTimeUtcFast); } [Fact] public void LengthOnDirectory() { - Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(Path.GetTempPath()).Length; }); + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(TestPath(Path.GetTempPath())).Length; }); } [Fact] @@ -365,7 +371,7 @@ public void DoesNotExistLastWriteTime() { string file = Guid.NewGuid().ToString("N"); - Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(file).LastWriteTime); + Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(TestPath(file)).LastWriteTime); } [Fact] @@ -375,7 +381,7 @@ public void DoesNotExistLastWriteTimeUtc() { string file = Guid.NewGuid().ToString("N"); - Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(file).LastWriteTimeUtcFast); + Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(TestPath(file)).LastWriteTimeUtcFast); } [Fact] @@ -383,7 +389,7 @@ public void DoesNotExistLength() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(file).Length; }); + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(TestPath(file)).Length; }); } [Fact] @@ -393,7 +399,7 @@ public void DoesNotExistIsDirectory() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - var x = new FileState(file).IsDirectory; + var x = new FileState(TestPath(file)).IsDirectory; }); } [Fact] @@ -401,7 +407,7 @@ public void DoesNotExistDirectoryOrFileExists() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - Assert.Equal(Directory.Exists(file), new FileState(file).DirectoryExists); + Assert.Equal(Directory.Exists(file), new FileState(TestPath(file)).DirectoryExists); } [Fact] @@ -409,8 +415,8 @@ public void DoesNotExistParentFolderNotFound() { string file = Guid.NewGuid().ToString("N") + "\\x"; // presumably doesn't exist - Assert.False(new FileState(file).FileExists); - Assert.False(new FileState(file).DirectoryExists); + Assert.False(new FileState(TestPath(file)).FileExists); + Assert.False(new FileState(TestPath(file)).DirectoryExists); } } } diff --git a/src/Tasks.UnitTests/FindAppConfigFile_Tests.cs b/src/Tasks.UnitTests/FindAppConfigFile_Tests.cs index 0965bb7c461..7f6a1a10600 100644 --- a/src/Tasks.UnitTests/FindAppConfigFile_Tests.cs +++ b/src/Tasks.UnitTests/FindAppConfigFile_Tests.cs @@ -48,7 +48,7 @@ public void FoundInSecondBelowProjectDirectory() f.SecondaryList = new ITaskItem[] { new TaskItem("foo\\app.config"), new TaskItem("xxx") }; f.TargetPath = "targetpath"; Assert.True(f.Execute()); - Assert.Equal(FileUtilities.FixFilePath("foo\\app.config"), f.AppConfigFile.ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath("foo\\app.config"), f.AppConfigFile.ItemSpec); Assert.Equal("targetpath", f.AppConfigFile.GetMetadata("TargetPath")); } @@ -74,7 +74,7 @@ public void MatchFileNameOnlyWithAnInvalidPath() f.TargetPath = "targetpath"; Assert.True(f.Execute()); // Should ignore the invalid paths - Assert.Equal(FileUtilities.FixFilePath(@"foo\\app.config"), f.AppConfigFile.ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\\app.config"), f.AppConfigFile.ItemSpec); } // For historical reasons, we should return the last one in the list diff --git a/src/Tasks.UnitTests/FindInList_Tests.cs b/src/Tasks.UnitTests/FindInList_Tests.cs index c051964fdfa..6cdece8d848 100644 --- a/src/Tasks.UnitTests/FindInList_Tests.cs +++ b/src/Tasks.UnitTests/FindInList_Tests.cs @@ -117,7 +117,7 @@ public void MatchFileNameOnly() f.MatchFileNameOnly = true; f.List = new ITaskItem[] { new TaskItem(@"c:\foo\a.cs"), new TaskItem("b.cs") }; Assert.True(f.Execute()); - Assert.Equal(FileUtilities.FixFilePath(@"c:\foo\a.cs"), f.ItemFound.ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"c:\foo\a.cs"), f.ItemFound.ItemSpec); } [Fact] @@ -132,7 +132,7 @@ public void MatchFileNameOnlyWithAnInvalidPath() Assert.True(f.Execute()); Console.WriteLine(e.Log); // Should ignore the invalid paths - Assert.Equal(FileUtilities.FixFilePath(@"foo\a.cs"), f.ItemFound.ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"foo\a.cs"), f.ItemFound.ItemSpec); } } } diff --git a/src/Tasks.UnitTests/FindUnderPath_Tests.cs b/src/Tasks.UnitTests/FindUnderPath_Tests.cs index 5b38b678b58..614a83b2202 100644 --- a/src/Tasks.UnitTests/FindUnderPath_Tests.cs +++ b/src/Tasks.UnitTests/FindUnderPath_Tests.cs @@ -31,8 +31,8 @@ public void BasicFilter() Assert.True(success); Assert.Single(t.InPath); Assert.Single(t.OutOfPath); - Assert.Equal(FileUtilities.FixFilePath(@"C:\MyProject\File1.txt"), t.InPath[0].ItemSpec); - Assert.Equal(FileUtilities.FixFilePath(@"C:\SomeoneElsesProject\File2.txt"), t.OutOfPath[0].ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"C:\MyProject\File1.txt"), t.InPath[0].ItemSpec); + Assert.Equal(FrameworkFileUtilities.FixFilePath(@"C:\SomeoneElsesProject\File2.txt"), t.OutOfPath[0].ItemSpec); } [WindowsFullFrameworkOnlyFact(additionalMessage: ".NET Core 2.1+ no longer validates paths: https://github.com/dotnet/corefx/issues/27779#issuecomment-371253486. On Unix there is no invalid file name characters.")] diff --git a/src/Tasks.UnitTests/MakeDir_Tests.cs b/src/Tasks.UnitTests/MakeDir_Tests.cs index 7b29328f258..c9cc65b0085 100644 --- a/src/Tasks.UnitTests/MakeDir_Tests.cs +++ b/src/Tasks.UnitTests/MakeDir_Tests.cs @@ -29,6 +29,7 @@ public void AttributeForwarding() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -76,6 +77,7 @@ public void SomeInputsFailToCreate() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -133,6 +135,7 @@ public void CreateNewDirectory() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -183,6 +186,7 @@ public void QuestionCreateNewDirectory() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Directories = dirList; @@ -199,6 +203,7 @@ public void QuestionCreateNewDirectory() engine.Log = ""; t = new MakeDir(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = dirList; success = t.Execute(); Assert.True(success); @@ -241,6 +246,7 @@ public void FileAlreadyExists() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs b/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs index 689c4f7f1aa..267cb7a756b 100644 --- a/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs +++ b/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs @@ -31,6 +31,7 @@ public void Basic() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("Line1") } }; @@ -39,6 +40,7 @@ public void Basic() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -78,6 +80,7 @@ public void Escaping() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("Line1_%253b_") } }; @@ -86,6 +89,7 @@ public void Escaping() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -123,6 +127,7 @@ public void ANSINonASCII() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("My special character is \u00C3") } }; @@ -131,6 +136,7 @@ public void ANSINonASCII() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -155,7 +161,8 @@ public void ReadMissing() // Read the line from the file. var r = new ReadLinesFromFile { - File = new TaskItem(file) + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -175,6 +182,7 @@ public void IgnoreBlankLines() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { @@ -191,6 +199,7 @@ public void IgnoreBlankLines() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -225,6 +234,7 @@ public void ReadNoAccess() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("This is a new line") } }; @@ -240,6 +250,7 @@ public void ReadNoAccess() var r = new ReadLinesFromFile(); var mEngine = new MockEngine(); r.BuildEngine = mEngine; + r.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); r.File = new TaskItem(file); Assert.False(r.Execute()); } diff --git a/src/Tasks.UnitTests/RemoveDir_Tests.cs b/src/Tasks.UnitTests/RemoveDir_Tests.cs index c0dd5b24cc2..5e01b3a91de 100644 --- a/src/Tasks.UnitTests/RemoveDir_Tests.cs +++ b/src/Tasks.UnitTests/RemoveDir_Tests.cs @@ -36,6 +36,7 @@ public void AttributeForwarding() i.SetMetadata("Locale", "en-GB"); t.Directories = new ITaskItem[] { i }; t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Execute(); @@ -61,6 +62,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), FailIfNotIncremental = true, }; t.Execute().ShouldBeFalse(); @@ -69,6 +71,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), }; t2.Execute().ShouldBeTrue(); t2.RemovedDirectories.Length.ShouldBe(list.Count); @@ -83,6 +86,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), FailIfNotIncremental = true, }; t3.Execute().ShouldBeTrue(); @@ -108,6 +112,7 @@ public void DeleteEmptyDirectory_WarnsAndContinues() RemoveDir t = new RemoveDir(); t.Directories = list.ToArray(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Execute().ShouldBeTrue(); t.RemovedDirectories.Length.ShouldBe(0); diff --git a/src/Tasks.UnitTests/TelemetryTaskTests.cs b/src/Tasks.UnitTests/TelemetryTaskTests.cs index db374f1757d..4a1c82aeaa4 100644 --- a/src/Tasks.UnitTests/TelemetryTaskTests.cs +++ b/src/Tasks.UnitTests/TelemetryTaskTests.cs @@ -90,11 +90,9 @@ public void TelemetryTaskDuplicateEventDataProperty() Assert.True(retVal); // Should not contain the first value - // Assert.DoesNotContain("EE2493A167D24F00996DE7C8E769EAE6", engine.Log); // Should contain the second value - // Assert.Contains("4ADE3D2622CA400B8B95A039DF540037", engine.Log); } } diff --git a/src/Tasks.UnitTests/Touch_Tests.cs b/src/Tasks.UnitTests/Touch_Tests.cs index 7f6da5b32ee..89a9a7c8165 100644 --- a/src/Tasks.UnitTests/Touch_Tests.cs +++ b/src/Tasks.UnitTests/Touch_Tests.cs @@ -187,6 +187,7 @@ public void TouchExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Files = new ITaskItem[] { @@ -210,6 +211,7 @@ public void TouchNonExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Files = new ITaskItem[] { @@ -232,6 +234,7 @@ public void TouchNonExistingAlwaysCreate() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -255,6 +258,7 @@ public void TouchNonExistingAlwaysCreateAndBadlyFormedTimestamp() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.ForceTouch = false; t.Time = "Badly formed time String."; @@ -278,6 +282,7 @@ public void TouchReadonly() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -300,6 +305,7 @@ public void TouchReadonlyForce() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.ForceTouch = true; t.AlwaysCreate = true; @@ -317,6 +323,7 @@ public void TouchNonExistingDirectoryDoesntExist() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -342,6 +349,7 @@ public void QuestionTouchNonExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Files = new ITaskItem[] @@ -368,6 +376,7 @@ public void QuestionTouchNonExistingAlwaysCreate() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -393,6 +402,7 @@ public void QuestionTouchExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Files = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs index 48ee8f14cce..a712746c098 100644 --- a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs +++ b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs @@ -38,6 +38,7 @@ public void InvalidEncoding() var a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), Encoding = "||invalid||", File = new TaskItem("c:\\" + Guid.NewGuid().ToString()), Lines = new TaskItem[] { new TaskItem("x") } @@ -61,6 +62,7 @@ public void Encoding() var a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("\uBDEA") } }; @@ -68,7 +70,8 @@ public void Encoding() var r = new ReadLinesFromFile { - File = new TaskItem(file) + File = new TaskItem(file), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; Assert.True(r.Execute()); @@ -80,6 +83,7 @@ public void Encoding() a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("\uBDEA") }, Encoding = "ASCII" @@ -89,7 +93,8 @@ public void Encoding() // Read the line from the file. r = new ReadLinesFromFile { - File = new TaskItem(file) + File = new TaskItem(file), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; Assert.True(r.Execute()); @@ -112,6 +117,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -120,7 +126,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() a.Execute().ShouldBeTrue(); // Verify contents - var r = new ReadLinesFromFile { File = new TaskItem(file) }; + var r = new ReadLinesFromFile { File = new TaskItem(file), TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; r.Execute().ShouldBeTrue(); r.Lines[0].ItemSpec.ShouldBe("File contents1"); @@ -133,6 +139,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -145,6 +152,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents2") } @@ -171,6 +179,7 @@ public void RedundantParametersAreLogged() WriteLinesToFile task = new() { BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem($"{nameof(RedundantParametersAreLogged)} Test") }, WriteOnlyWhenDifferent = true, @@ -195,6 +204,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -203,7 +213,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() a.Execute().ShouldBeTrue(); // Verify contents - var r = new ReadLinesFromFile { File = new TaskItem(file) }; + var r = new ReadLinesFromFile { File = new TaskItem(file), TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; r.Execute().ShouldBeTrue(); r.Lines[0].ItemSpec.ShouldBe("File contents1"); @@ -216,6 +226,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") }, @@ -229,6 +240,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents2") }, @@ -274,6 +286,7 @@ void TestWriteLines(string fileExists, string fileNotExists, bool Overwrite, boo { Overwrite = Overwrite, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(fileExists), WriteOnlyWhenDifferent = WriteOnlyWhenDifferent, FailIfNotIncremental = true, @@ -285,6 +298,7 @@ void TestWriteLines(string fileExists, string fileNotExists, bool Overwrite, boo { Overwrite = Overwrite, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(fileNotExists), WriteOnlyWhenDifferent = WriteOnlyWhenDifferent, FailIfNotIncremental = true, @@ -308,6 +322,7 @@ public void WriteLinesToFileDoesCreateDirectory() var WriteLinesToFile = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("WriteLinesToFileDoesCreateDirectory Test") } }; @@ -339,6 +354,7 @@ public void WritingNothingErasesExistingFile(bool useNullLines) { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file.Path), Lines = lines }.Execute().ShouldBeTrue(); @@ -365,6 +381,7 @@ public void WritingNothingCreatesNewFile(bool useNullLines) { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file.Path), Lines = lines }.Execute().ShouldBeTrue(); diff --git a/src/Tasks/AssemblyDependency/ReferenceTable.cs b/src/Tasks/AssemblyDependency/ReferenceTable.cs index ea9ccab8e6c..1c68ec4d840 100644 --- a/src/Tasks/AssemblyDependency/ReferenceTable.cs +++ b/src/Tasks/AssemblyDependency/ReferenceTable.cs @@ -1720,7 +1720,7 @@ private bool FindAssociatedFiles() { // We don't look for associated files for FX assemblies. bool hasFrameworkPath = false; - string referenceDirectoryName = FileUtilities.EnsureTrailingSlash(reference.DirectoryName); + string referenceDirectoryName = FrameworkFileUtilities.EnsureTrailingSlash(reference.DirectoryName); foreach (string frameworkPath in _frameworkPaths) { @@ -2768,7 +2768,7 @@ private ITaskItem SetItemMetadata(List relatedItems, List // Set up the satellites. foreach (string satelliteFile in satellites) { - relatedItemBase.SetMetadata(ItemMetadataNames.destinationSubDirectory, FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(satelliteFile))); + relatedItemBase.SetMetadata(ItemMetadataNames.destinationSubDirectory, FrameworkFileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(satelliteFile))); AddRelatedItem(satelliteItems, relatedItemBase, Path.Combine(reference.DirectoryName, satelliteFile)); } } diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index 30d999a4ec4..9c685aa4bbb 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -2246,7 +2246,7 @@ internal bool Execute( { for (int i = 0; i < _targetFrameworkDirectories.Length; i++) { - _targetFrameworkDirectories[i] = FileUtilities.EnsureTrailingSlash(_targetFrameworkDirectories[i]); + _targetFrameworkDirectories[i] = FrameworkFileUtilities.EnsureTrailingSlash(_targetFrameworkDirectories[i]); } } diff --git a/src/Tasks/AssignTargetPath.cs b/src/Tasks/AssignTargetPath.cs index 1c9ca3d5c38..5e3cda7c794 100644 --- a/src/Tasks/AssignTargetPath.cs +++ b/src/Tasks/AssignTargetPath.cs @@ -55,7 +55,7 @@ public override bool Execute() string fullRootPath = Path.GetFullPath(RootFolder); // Ensure trailing slash otherwise c:\bin appears to match part of c:\bin2\foo - fullRootPath = FileUtilities.EnsureTrailingSlash(fullRootPath); + fullRootPath = FrameworkFileUtilities.EnsureTrailingSlash(fullRootPath); string currentDirectory = Directory.GetCurrentDirectory(); diff --git a/src/Tasks/CombineTargetFrameworkInfoProperties.cs b/src/Tasks/CombineTargetFrameworkInfoProperties.cs index 7506fbc8be0..e2a2f6baa37 100644 --- a/src/Tasks/CombineTargetFrameworkInfoProperties.cs +++ b/src/Tasks/CombineTargetFrameworkInfoProperties.cs @@ -12,6 +12,7 @@ namespace Microsoft.Build.Tasks /// /// Combines items that represent properties and values into an XML representation. /// + [MSBuildMultiThreadableTask] public class CombineTargetFrameworkInfoProperties : TaskExtension { /// diff --git a/src/Tasks/CombineXmlElements.cs b/src/Tasks/CombineXmlElements.cs index 8fb5af15dc2..04b973b4e79 100644 --- a/src/Tasks/CombineXmlElements.cs +++ b/src/Tasks/CombineXmlElements.cs @@ -11,6 +11,7 @@ namespace Microsoft.Build.Tasks /// /// Combines multiple XML elements /// + [MSBuildMultiThreadableTask] public class CombineXmlElements : TaskExtension { /// diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index 208bb6ca298..a3adcf74260 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -21,7 +21,8 @@ namespace Microsoft.Build.Tasks /// /// A task that copies files. /// - public class Copy : TaskExtension, IIncrementalTask, ICancelableTask + [MSBuildMultiThreadableTask] + public class Copy : TaskExtension, IIncrementalTask, ICancelableTask, IMultiThreadableTask { internal const string AlwaysRetryEnvVar = "MSBUILDALWAYSRETRY"; internal const string AlwaysOverwriteReadOnlyFilesEnvVar = "MSBUILDALWAYSOVERWRITEREADONLYFILES"; @@ -185,6 +186,9 @@ public Copy() public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + #endregion /// @@ -260,7 +264,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p { if (destinationFileState.DirectoryExists) { - Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue); return false; } @@ -270,17 +274,18 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p // error telling the user so. Otherwise, .NET Framework's File.Copy method will throw // an UnauthorizedAccessException saying "access is denied", which is not very useful // to the user. - Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Path.OriginalValue); return false; } if (!sourceFileState.FileExists) { - Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Path.OriginalValue); return false; } - string destinationFolder = Path.GetDirectoryName(destinationFileState.Name); + string destinationFolder = Path.GetDirectoryName(destinationFileState.Path); + string originalDestinationFolder = Path.GetDirectoryName(destinationFileState.Path.OriginalValue); if (!string.IsNullOrEmpty(destinationFolder) && !_directoriesKnownToExist.ContainsKey(destinationFolder)) { @@ -288,12 +293,12 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p { if (FailIfNotIncremental) { - Log.LogError(CreatesDirectory, destinationFolder); + Log.LogError(CreatesDirectory, originalDestinationFolder); return false; } else { - Log.LogMessage(MessageImportance.Normal, CreatesDirectory, destinationFolder); + Log.LogMessage(MessageImportance.Normal, CreatesDirectory, originalDestinationFolder); Directory.CreateDirectory(destinationFolder); } } @@ -306,7 +311,8 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (FailIfNotIncremental) { - Log.LogError(FileComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior + Log.LogError(FileComment, sourceFileState.Path, destinationFileState.Path); return false; } @@ -319,7 +325,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p destinationFileState.FileExists && !destinationFileState.IsReadOnly) { - FileUtilities.DeleteNoThrow(destinationFileState.Name); + FileUtilities.DeleteNoThrow(destinationFileState.Path); } bool symbolicLinkCreated = false; @@ -335,11 +341,12 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (UseSymboliclinksIfPossible) { // This is a message for fallback to SymbolicLinks if HardLinks fail when UseHardlinksIfPossible and UseSymboliclinksIfPossible are true - Log.LogMessage(MessageImportance.Normal, RetryingAsSymbolicLink, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior + Log.LogMessage(MessageImportance.Normal, RetryingAsSymbolicLink, sourceFileState.Path, destinationFileState.Path, errorMessage); } else { - Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Path, destinationFileState.Path, errorMessage); } } } @@ -355,13 +362,13 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage); } - Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Path, destinationFileState.Path, errorMessage); } } if (ErrorIfLinkFails && !hardLinkCreated && !symbolicLinkCreated) { - Log.LogErrorWithCodeFromResources("Copy.LinkFailed", sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogErrorWithCodeFromResources("Copy.LinkFailed", sourceFileState.Path, destinationFileState.Path); return false; } @@ -370,9 +377,9 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (!hardLinkCreated && !symbolicLinkCreated) { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessage(MessageImportance.Normal, FileComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogMessage(MessageImportance.Normal, FileComment, sourceFileState.Path, destinationFileState.Path); - File.Copy(sourceFileState.Name, destinationFileState.Name, true); + File.Copy(sourceFileState.Path, destinationFileState.Path, true); } // If the destinationFile file exists, then make sure it's read-write. @@ -393,9 +400,9 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, out bool linkCreated, ref string errorMessage, Func createLink) { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.Path, destinationFileState.Path); - linkCreated = createLink(sourceFileState.Name, destinationFileState.Name, errorMessage); + linkCreated = createLink(sourceFileState.Path, destinationFileState.Path, errorMessage); } /// @@ -410,10 +417,10 @@ private void MakeFileWriteable(FileState file, bool logActivity) { if (logActivity) { - Log.LogMessage(MessageImportance.Low, RemovingReadOnlyAttribute, file.Name); + Log.LogMessage(MessageImportance.Low, RemovingReadOnlyAttribute, file.Path.OriginalValue); } - File.SetAttributes(file.Name, FileAttributes.Normal); + File.SetAttributes(file.Path, FileAttributes.Normal); file.Reset(); } } @@ -444,7 +451,7 @@ internal bool Execute( } // Environment variable stomps on user-requested value if it's set. - if (Environment.GetEnvironmentVariable(AlwaysOverwriteReadOnlyFilesEnvVar) != null) + if (TaskEnvironment.GetEnvironmentVariable(AlwaysOverwriteReadOnlyFilesEnvVar) != null) { OverwriteReadOnlyFiles = true; } @@ -497,11 +504,17 @@ private bool CopySingleThreaded( for (int i = 0; i < SourceFiles.Length && !_cancellationTokenSource.IsCancellationRequested; ++i) { bool copyComplete = false; - string destPath = DestinationFiles[i].ItemSpec; - MSBuildEventSource.Log.CopyUpToDateStart(destPath); - if (filesActuallyCopied.TryGetValue(destPath, out string originalSource)) + string sourceSpec = SourceFiles[i].ItemSpec; + string destSpec = DestinationFiles[i].ItemSpec; + + // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + + MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); + if (filesActuallyCopied.TryGetValue(destAbsolutePath, out string originalSource)) { - if (String.Equals(originalSource, SourceFiles[i].ItemSpec, FileUtilities.PathComparison)) + if (originalSource == sourceAbsolutePath) { // Already copied from this location, don't copy again. copyComplete = true; @@ -510,9 +523,9 @@ private bool CopySingleThreaded( if (!copyComplete) { - if (DoCopyIfNecessary(new FileState(SourceFiles[i].ItemSpec), new FileState(DestinationFiles[i].ItemSpec), copyFile)) + if (DoCopyIfNecessary(new FileState(sourceAbsolutePath), new FileState(destAbsolutePath), copyFile)) { - filesActuallyCopied[destPath] = SourceFiles[i].ItemSpec; + filesActuallyCopied[destAbsolutePath] = sourceAbsolutePath; copyComplete = true; } else @@ -522,7 +535,7 @@ private bool CopySingleThreaded( } else { - MSBuildEventSource.Log.CopyUpToDateStop(destPath, true); + MSBuildEventSource.Log.CopyUpToDateStop(destAbsolutePath.OriginalValue, true); } if (copyComplete) @@ -638,26 +651,35 @@ void ProcessPartition() { while (partitionQueue.TryDequeue(out List partition)) { + // Cache the previous source absolute path to avoid recomputing it + AbsolutePath prevSourceAbsolutePath = default; + for (int partitionIndex = 0; partitionIndex < partition.Count && !_cancellationTokenSource.IsCancellationRequested; partitionIndex++) { int fileIndex = partition[partitionIndex]; ITaskItem sourceItem = SourceFiles[fileIndex]; ITaskItem destItem = DestinationFiles[fileIndex]; - string sourcePath = sourceItem.ItemSpec; + string sourceSpec = sourceItem.ItemSpec; + string destSpec = destItem.ItemSpec; + // Compute absolute paths once - reused for ETW, deduplication check, and FileState + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + // Check if we just copied from this location to the destination, don't copy again. - MSBuildEventSource.Log.CopyUpToDateStart(destItem.ItemSpec); - bool copyComplete = partitionIndex > 0 && - String.Equals( - sourcePath, - SourceFiles[partition[partitionIndex - 1]].ItemSpec, - FileUtilities.PathComparison); + MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); + bool copyComplete = false; + if (partitionIndex > 0) + { + // Use cached absolute path from previous iteration instead of recomputing + copyComplete = sourceAbsolutePath == prevSourceAbsolutePath; + } if (!copyComplete) { if (DoCopyIfNecessary( - new FileState(sourceItem.ItemSpec), - new FileState(destItem.ItemSpec), + new FileState(sourceAbsolutePath), + new FileState(destAbsolutePath), copyFile)) { copyComplete = true; @@ -670,7 +692,7 @@ void ProcessPartition() } else { - MSBuildEventSource.Log.CopyUpToDateStop(destItem.ItemSpec, true); + MSBuildEventSource.Log.CopyUpToDateStop(destAbsolutePath.OriginalValue, true); } if (copyComplete) @@ -678,6 +700,9 @@ void ProcessPartition() sourceItem.CopyMetadataTo(destItem); successFlags[fileIndex] = (IntPtr)1; } + + // Cache for next iteration's duplicate check + prevSourceAbsolutePath = sourceAbsolutePath; } } } @@ -799,7 +824,8 @@ private bool InitializeDestinationFiles() foreach (ITaskItem sourceFolder in SourceFolders) { - string src = FileUtilities.NormalizePath(sourceFolder.ItemSpec); + ErrorUtilities.VerifyThrowArgumentLength(sourceFolder.ItemSpec); + AbsolutePath src = FrameworkFileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(sourceFolder.ItemSpec)); string srcName = Path.GetFileName(src); (string[] filesInFolder, _, _, string globFailure) = FileMatcher.Default.GetFiles(src, "**"); @@ -900,19 +926,18 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF Log.LogMessage( MessageImportance.Low, DidNotCopyBecauseOfFileMatch, - sourceFileState.Name, - destinationFileState.Name, + sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, "SkipUnchangedFiles", "true"); - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, true); + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, true); } else if (!PathsAreIdentical(sourceFileState, destinationFileState)) { - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, false); - + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, false); if (FailIfNotIncremental) { - Log.LogError(FileComment, sourceFileState.Name, destinationFileState.Name); + Log.LogError(FileComment, sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue); success = false; } else @@ -922,7 +947,7 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF } else { - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, true); + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, true); } } catch (OperationCanceledException) @@ -931,12 +956,12 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF } catch (PathTooLongException e) { - Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, e.Message); success = false; } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, e.Message); success = false; } @@ -976,7 +1001,7 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF case IOException: // Not clear why we can get one and not the other int code = Marshal.GetHRForException(e); - LogAlwaysRetryDiagnosticFromResources("Copy.IOException", e.ToString(), sourceFileState.Name, destinationFileState.Name, code); + LogAlwaysRetryDiagnosticFromResources("Copy.IOException", e.ToString(), sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, code); if (code == NativeMethods.ERROR_ACCESS_DENIED) { // ERROR_ACCESS_DENIED can either mean there's an ACL preventing us, or the file has the readonly bit set. @@ -1006,7 +1031,7 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF break; } - if (DestinationFolder != null && FileSystems.Default.FileExists(DestinationFolder.ItemSpec)) + if (DestinationFolder != null && FileSystems.Default.FileExists(TaskEnvironment.GetAbsolutePath(DestinationFolder.ItemSpec))) { // We failed to create the DestinationFolder because it's an existing file. No sense retrying. // We don't check for this case upstream because it'd be another hit to the filesystem. @@ -1019,9 +1044,9 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, - destinationFileState.Name, retries, RetryDelayMilliseconds, e.Message, - LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, retries, RetryDelayMilliseconds, e.Message, + LockCheck.GetLockedFileMessage(destinationFileState.Path)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); @@ -1032,8 +1057,8 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF else if (Retries > 0) { // Exception message is logged in caller - Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, - destinationFileState.Name, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Path)); throw; } else @@ -1045,9 +1070,9 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, - destinationFileState.Name, retries, RetryDelayMilliseconds, String.Empty /* no details */, - LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, retries, RetryDelayMilliseconds, String.Empty /* no details */, + LockCheck.GetLockedFileMessage(destinationFileState.Path)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); @@ -1056,8 +1081,8 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF } else if (Retries > 0) { - Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, - destinationFileState.Name, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Path)); return false; } else @@ -1085,16 +1110,16 @@ public override bool Execute() /// Compares two paths to see if they refer to the same file. We can't solve the general /// canonicalization problem, so we just compare strings on the full paths. /// + /// + /// This method has a side effect of removing relative segments from the paths before comparison to avoid + /// false negatives due to different path representations. This operation may throw in certain cases (e.g. invalid paths on Windows). + /// TODO: refactor this task not to rely on this side effect for correct exception handling and caching + /// private static bool PathsAreIdentical(FileState source, FileState destination) { - if (string.Equals(source.Name, destination.Name, FileUtilities.PathComparison)) - { - return true; - } - - source.FileNameFullPath = Path.GetFullPath(source.Name); - destination.FileNameFullPath = Path.GetFullPath(destination.Name); - return string.Equals(source.FileNameFullPath, destination.FileNameFullPath, FileUtilities.PathComparison); + source.Path = FrameworkFileUtilities.RemoveRelativeSegments(source.Path); + destination.Path = FrameworkFileUtilities.RemoveRelativeSegments(destination.Path); + return source.Path == destination.Path; } private static bool GetParallelismFromEnvironment() diff --git a/src/Tasks/CreateCSharpManifestResourceName.cs b/src/Tasks/CreateCSharpManifestResourceName.cs index c7f838b16ef..85a5b1b1ec2 100644 --- a/src/Tasks/CreateCSharpManifestResourceName.cs +++ b/src/Tasks/CreateCSharpManifestResourceName.cs @@ -97,13 +97,13 @@ internal static string CreateManifestNameImpl( bool enableCustomCulture = false) { // Use the link file name if there is one, otherwise, fall back to file name. - string embeddedFileName = FileUtilities.FixFilePath(linkFileName); + string embeddedFileName = FrameworkFileUtilities.FixFilePath(linkFileName); if (string.IsNullOrEmpty(embeddedFileName)) { - embeddedFileName = FileUtilities.FixFilePath(fileName); + embeddedFileName = FrameworkFileUtilities.FixFilePath(fileName); } - dependentUponFileName = FileUtilities.FixFilePath(dependentUponFileName); + dependentUponFileName = FrameworkFileUtilities.FixFilePath(dependentUponFileName); Culture.ItemCultureInfo info; if (!string.IsNullOrEmpty(culture) && enableCustomCulture) diff --git a/src/Tasks/CreateVisualBasicManifestResourceName.cs b/src/Tasks/CreateVisualBasicManifestResourceName.cs index d2cf7f405ef..24c59241ee0 100644 --- a/src/Tasks/CreateVisualBasicManifestResourceName.cs +++ b/src/Tasks/CreateVisualBasicManifestResourceName.cs @@ -102,7 +102,7 @@ internal static string CreateManifestNameImpl( embeddedFileName = fileName; } - dependentUponFileName = FileUtilities.FixFilePath(dependentUponFileName); + dependentUponFileName = FrameworkFileUtilities.FixFilePath(dependentUponFileName); Culture.ItemCultureInfo info; if (!string.IsNullOrEmpty(culture) && enableCustomCulture) diff --git a/src/Tasks/Delete.cs b/src/Tasks/Delete.cs index e10ad4f733f..d687b9c0006 100644 --- a/src/Tasks/Delete.cs +++ b/src/Tasks/Delete.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// Delete files from disk. /// - public class Delete : TaskExtension, ICancelableTask, IIncrementalTask + [MSBuildMultiThreadableTask] + public class Delete : TaskExtension, ICancelableTask, IIncrementalTask, IMultiThreadableTask { #region Properties @@ -63,6 +64,9 @@ public ITaskItem[] Files /// public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Verify that the inputs are correct. /// @@ -115,27 +119,30 @@ public override bool Execute() } int retries = 0; + // deletedFilesSet is not normalized to save time on allocation while (!deletedFilesSet.Contains(file.ItemSpec)) { + AbsolutePath? filePath = null; try { - if (FileSystems.Default.FileExists(file.ItemSpec)) + filePath = TaskEnvironment.GetAbsolutePath(file.ItemSpec); + if (FileSystems.Default.FileExists(filePath)) { if (FailIfNotIncremental) { - Log.LogWarningFromResources("Delete.DeletingFile", file.ItemSpec); + Log.LogWarningFromResources("Delete.DeletingFile", filePath.Value.OriginalValue); } else { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", file.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", filePath.Value.OriginalValue); } - File.Delete(file.ItemSpec); + File.Delete(filePath); } else { - Log.LogMessageFromResources(MessageImportance.Low, "Delete.SkippingNonexistentFile", file.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Low, "Delete.SkippingNonexistentFile", filePath.Value.OriginalValue); } // keep a running list of the files that were actually deleted // note that we include in this list files that did not exist @@ -146,18 +153,25 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - string lockedFileMessage = LockCheck.GetLockedFileMessage(file?.ItemSpec ?? string.Empty); + string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Delete.Retrying", file.ToString(), retries, RetryDelayMilliseconds, e.Message, lockedFileMessage); + Log.LogWarningWithCodeFromResources("Delete.Retrying", filePath?.OriginalValue ?? file.ItemSpec, retries, RetryDelayMilliseconds, e.Message, lockedFileMessage); Thread.Sleep(RetryDelayMilliseconds); continue; } else { - LogError(file, e, lockedFileMessage); + if (TreatErrorsAsWarnings) + { + Log.LogWarningWithCodeFromResources("Delete.Error", filePath?.OriginalValue ?? file.ItemSpec, e.Message, lockedFileMessage); + } + else + { + Log.LogErrorWithCodeFromResources("Delete.Error", filePath?.OriginalValue ?? file.ItemSpec, e.Message, lockedFileMessage); + } // Add on failure to avoid reattempting deletedFilesSet.Add(file.ItemSpec); } @@ -168,25 +182,6 @@ public override bool Execute() DeletedFiles = deletedFilesList.ToArray(); return !Log.HasLoggedErrors; } - - /// - /// Log an error. - /// - /// The file that wasn't deleted. - /// The exception. - /// Message from . - private void LogError(ITaskItem file, Exception e, string lockedFileMessage) - { - if (TreatErrorsAsWarnings) - { - Log.LogWarningWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message, lockedFileMessage); - } - else - { - Log.LogErrorWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message, lockedFileMessage); - } - } - #endregion } } diff --git a/src/Tasks/DependencyFile.cs b/src/Tasks/DependencyFile.cs index a5a2c3ec528..0e591aa7f97 100644 --- a/src/Tasks/DependencyFile.cs +++ b/src/Tasks/DependencyFile.cs @@ -3,7 +3,7 @@ using System; using System.IO; - +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -61,7 +61,7 @@ internal bool Exists /// The file name. internal DependencyFile(string filename) { - this.filename = FileUtilities.FixFilePath(filename); + this.filename = FrameworkFileUtilities.FixFilePath(filename); if (FileSystems.Default.FileExists(FileName)) { diff --git a/src/Tasks/Error.cs b/src/Tasks/Error.cs index fc7d5cf2288..40f9c7edf1c 100644 --- a/src/Tasks/Error.cs +++ b/src/Tasks/Error.cs @@ -3,9 +3,7 @@ #nullable disable -#if NET using System.Diagnostics.CodeAnalysis; -#endif using Microsoft.Build.Framework; namespace Microsoft.Build.Tasks diff --git a/src/Tasks/ErrorFromResources.cs b/src/Tasks/ErrorFromResources.cs index a95a8ca2fd6..f931c5ebd15 100644 --- a/src/Tasks/ErrorFromResources.cs +++ b/src/Tasks/ErrorFromResources.cs @@ -13,6 +13,7 @@ namespace Microsoft.Build.Tasks /// Task that emits an error given a resource string. Engine will add project file path and line/column /// information. /// + [MSBuildMultiThreadableTask] public sealed class ErrorFromResources : TaskExtension { /// diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index 4daa47cf647..cf87ebedec1 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -#if NET using System.Diagnostics.CodeAnalysis; -#endif using System.IO; using System.Text; using System.Text.RegularExpressions; diff --git a/src/Tasks/FileIO/ReadLinesFromFile.cs b/src/Tasks/FileIO/ReadLinesFromFile.cs index 86b5ca17068..1b80db354aa 100644 --- a/src/Tasks/FileIO/ReadLinesFromFile.cs +++ b/src/Tasks/FileIO/ReadLinesFromFile.cs @@ -15,7 +15,8 @@ namespace Microsoft.Build.Tasks /// /// Read a list of items from a file. /// - public class ReadLinesFromFile : TaskExtension + [MSBuildMultiThreadableTask] + public class ReadLinesFromFile : TaskExtension, IMultiThreadableTask { /// /// File to read lines from. @@ -23,6 +24,9 @@ public class ReadLinesFromFile : TaskExtension [Required] public ITaskItem File { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Receives lines from file. /// @@ -38,12 +42,12 @@ public override bool Execute() bool success = true; if (File != null) { - if (FileSystems.Default.FileExists(File.ItemSpec)) + AbsolutePath filePath = TaskEnvironment.GetAbsolutePath(File.ItemSpec); + if (FileSystems.Default.FileExists(filePath)) { try { - string[] textLines = System.IO.File.ReadAllLines(File.ItemSpec); - + string[] textLines = System.IO.File.ReadAllLines(filePath); var nonEmptyLines = new List(); char[] charsToTrim = { '\0', ' ', '\t' }; @@ -66,7 +70,7 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("ReadLinesFromFile.ErrorOrWarning", File.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("ReadLinesFromFile.ErrorOrWarning", filePath.OriginalValue, e.Message); success = false; } } diff --git a/src/Tasks/FileIO/WriteLinesToFile.cs b/src/Tasks/FileIO/WriteLinesToFile.cs index 15b93fc25f0..560264ad909 100644 --- a/src/Tasks/FileIO/WriteLinesToFile.cs +++ b/src/Tasks/FileIO/WriteLinesToFile.cs @@ -16,11 +16,15 @@ namespace Microsoft.Build.Tasks /// /// Appends a list of items to a file. One item per line with carriage returns in-between. /// - public class WriteLinesToFile : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class WriteLinesToFile : TaskExtension, IIncrementalTask, IMultiThreadableTask { // Default encoding taken from System.IO.WriteAllText() private static readonly Encoding s_defaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// File to write lines to. /// @@ -62,15 +66,13 @@ public class WriteLinesToFile : TaskExtension, IIncrementalTask /// public override bool Execute() { - bool success = true; - if (File == null) { - return success; + return true; } - string filePath = FileUtilities.NormalizePath(File.ItemSpec); - + ErrorUtilities.VerifyThrowArgumentLength(File.ItemSpec); + AbsolutePath filePath = FrameworkFileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(File.ItemSpec)); string contentsAsString = string.Empty; if (Lines != null && Lines.Length > 0) @@ -122,7 +124,7 @@ public override bool Execute() } } - private bool ExecuteNonTransactional(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool ExecuteNonTransactional(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -134,7 +136,7 @@ private bool ExecuteNonTransactional(string filePath, string directoryPath, stri { if (WriteOnlyWhenDifferent) { - Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath); + Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath.OriginalValue); } System.IO.File.AppendAllText(filePath, contentsAsString, encoding); @@ -145,12 +147,12 @@ private bool ExecuteNonTransactional(string filePath, string directoryPath, stri catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } - private bool ExecuteTransactional(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool ExecuteTransactional(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -162,7 +164,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string { if (WriteOnlyWhenDifferent) { - Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath); + Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath.OriginalValue); } // For append mode, use atomic write to append only the new content @@ -173,7 +175,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -182,7 +184,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string /// Saves content to file atomically using a temporary file, following the Visual Studio editor pattern. /// This is for overwrite mode where we write the entire content. /// - private bool SaveAtomically(string filePath, string contentsAsString, Encoding encoding) + private bool SaveAtomically(AbsolutePath filePath, string contentsAsString, Encoding encoding) { string temporaryFilePath = null; try @@ -217,7 +219,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e { // Move failed, log and return string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, moveEx.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -249,7 +251,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e catch (Exception fallbackEx) when (ExceptionHandling.IsIoRelatedException(fallbackEx)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, fallbackEx.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, fallbackEx.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -257,7 +259,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } finally @@ -284,7 +286,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e /// Appends content to file atomically. For append mode, we simply append the new content /// directly without reading the entire file, avoiding race conditions. /// - private bool SaveAtomicallyAppend(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool SaveAtomicallyAppend(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -297,7 +299,7 @@ private bool SaveAtomicallyAppend(string filePath, string directoryPath, string catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -306,7 +308,7 @@ private bool SaveAtomicallyAppend(string filePath, string directoryPath, string /// Checks if file should be written for Overwrite mode, considering WriteOnlyWhenDifferent option. /// /// True if file should be written, false if write should be skipped. - private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsString) + private bool ShouldWriteFileForOverwrite(AbsolutePath filePath, string contentsAsString) { if (!WriteOnlyWhenDifferent) { @@ -321,24 +323,24 @@ private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsStrin // Use stream-based comparison to avoid loading entire file into memory if (FilesAreIdentical(filePath, contentsAsString)) { - Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.SkippingUnchangedFile", filePath); - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, true); + Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.SkippingUnchangedFile", filePath.OriginalValue); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, true); return false; // Skip write - content is identical } else if (FailIfNotIncremental) { - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", filePath); - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, false); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", filePath.OriginalValue); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, false); return false; // Skip write - file differs and FailIfNotIncremental is set } } } catch (IOException) { - Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.ErrorReadingFile", filePath); + Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.ErrorReadingFile", filePath.OriginalValue); } - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, false); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, false); return true; // Proceed with write } @@ -347,7 +349,7 @@ private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsStrin /// Uses the default encoding for the comparison. /// /// True if file contents are identical to the provided string, false otherwise. - private bool FilesAreIdentical(string filePath, string contentsAsString) + private bool FilesAreIdentical(AbsolutePath filePath, string contentsAsString) { try { diff --git a/src/Tasks/FileState.cs b/src/Tasks/FileState.cs index 9021a946833..806dda2d893 100644 --- a/src/Tasks/FileState.cs +++ b/src/Tasks/FileState.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -226,16 +227,6 @@ public void ThrowException() } } - /// - /// The name of the file. - /// - private readonly string _filename; - - /// - /// Holds the full path equivalent of _filename - /// - public string FileNameFullPath; - /// /// Actual file or directory information /// @@ -245,11 +236,11 @@ public void ThrowException() /// Constructor. /// Only stores file name: does not grab the file state until first request. /// - internal FileState(string filename) + /// The normalized (absolute) path to the file. + internal FileState(AbsolutePath path) { - ErrorUtilities.VerifyThrowArgumentLength(filename); - _filename = filename; - _data = new Lazy(() => new FileDirInfo(_filename)); + Path = path; + _data = new Lazy(() => new FileDirInfo(Path)); } /// @@ -309,10 +300,18 @@ internal long Length } /// - /// Name of the file as it was passed in. - /// Not normalized. + /// Path of the file. /// - internal string Name => _filename; + internal AbsolutePath Path + { + get; + set + { + ErrorUtilities.VerifyThrowArgumentLength(value); + field = value; + _data = new Lazy(() => new FileDirInfo(value)); + } + } /// /// Whether this is a directory. @@ -333,7 +332,7 @@ internal bool IsDirectory /// internal void Reset() { - _data = new Lazy(() => new FileDirInfo(_filename)); + _data = new Lazy(() => new FileDirInfo(Path)); } } } diff --git a/src/Tasks/FindAppConfigFile.cs b/src/Tasks/FindAppConfigFile.cs index f8b1332e062..656a5308db0 100644 --- a/src/Tasks/FindAppConfigFile.cs +++ b/src/Tasks/FindAppConfigFile.cs @@ -15,6 +15,7 @@ namespace Microsoft.Build.Tasks /// For compat reasons, it has to follow a particular arbitrary algorithm. /// It also adds the TargetPath metadata. /// + [MSBuildMultiThreadableTask] public class FindAppConfigFile : TaskExtension { // The list to search through diff --git a/src/Tasks/FindInList.cs b/src/Tasks/FindInList.cs index 84538f53b6e..493990ca1a9 100644 --- a/src/Tasks/FindInList.cs +++ b/src/Tasks/FindInList.cs @@ -105,7 +105,7 @@ private bool IsMatchingItem(StringComparison comparison, ITaskItem item) { try { - var path = FileUtilities.FixFilePath(item.ItemSpec); + var path = FrameworkFileUtilities.FixFilePath(item.ItemSpec); string filename = (MatchFileNameOnly ? Path.GetFileName(path) : path); if (String.Equals(filename, ItemSpecToFind, comparison)) diff --git a/src/Tasks/FindInvalidProjectReferences.cs b/src/Tasks/FindInvalidProjectReferences.cs index 607df8b83cd..7c2359a9e45 100644 --- a/src/Tasks/FindInvalidProjectReferences.cs +++ b/src/Tasks/FindInvalidProjectReferences.cs @@ -13,6 +13,7 @@ namespace Microsoft.Build.Tasks /// /// Returns the reference assembly paths to the various frameworks /// + [MSBuildMultiThreadableTask] public partial class FindInvalidProjectReferences : TaskExtension { #region Fields diff --git a/src/Tasks/GetCompatiblePlatform.cs b/src/Tasks/GetCompatiblePlatform.cs index c66e8542878..802a264331d 100644 --- a/src/Tasks/GetCompatiblePlatform.cs +++ b/src/Tasks/GetCompatiblePlatform.cs @@ -14,6 +14,7 @@ namespace Microsoft.Build.Tasks /// /// See ProjectReference-Protocol.md for details. /// + [MSBuildMultiThreadableTask] public class GetCompatiblePlatform : TaskExtension { /// diff --git a/src/Tasks/GetFrameworkSDKPath.cs b/src/Tasks/GetFrameworkSDKPath.cs index fb90e032392..05f35b77b96 100644 --- a/src/Tasks/GetFrameworkSDKPath.cs +++ b/src/Tasks/GetFrameworkSDKPath.cs @@ -4,7 +4,6 @@ #if NETFRAMEWORK using System; -using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #endif @@ -56,7 +55,7 @@ public string Path } else { - s_path = FileUtilities.EnsureTrailingSlash(s_path); + s_path = FrameworkFileUtilities.EnsureTrailingSlash(s_path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_path); } } @@ -93,7 +92,7 @@ public string FrameworkSdkVersion20Path } else { - s_version20Path = FileUtilities.EnsureTrailingSlash(s_version20Path); + s_version20Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version20Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version20Path); } } @@ -126,7 +125,7 @@ public string FrameworkSdkVersion35Path } else { - s_version35Path = FileUtilities.EnsureTrailingSlash(s_version35Path); + s_version35Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version35Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version35Path); } } @@ -159,7 +158,7 @@ public string FrameworkSdkVersion40Path } else { - s_version40Path = FileUtilities.EnsureTrailingSlash(s_version40Path); + s_version40Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version40Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version40Path); } } @@ -192,7 +191,7 @@ public string FrameworkSdkVersion45Path } else { - s_version45Path = FileUtilities.EnsureTrailingSlash(s_version45Path); + s_version45Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version45Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version45Path); } } @@ -225,7 +224,7 @@ public string FrameworkSdkVersion451Path } else { - s_version451Path = FileUtilities.EnsureTrailingSlash(s_version451Path); + s_version451Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version451Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version451Path); } } @@ -258,7 +257,7 @@ public string FrameworkSdkVersion46Path } else { - s_version46Path = FileUtilities.EnsureTrailingSlash(s_version46Path); + s_version46Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version46Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version46Path); } } @@ -291,7 +290,7 @@ public string FrameworkSdkVersion461Path } else { - s_version461Path = FileUtilities.EnsureTrailingSlash(s_version461Path); + s_version461Path = FrameworkFileUtilities.EnsureTrailingSlash(s_version461Path); Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version461Path); } } diff --git a/src/Tasks/GetSDKReferenceFiles.cs b/src/Tasks/GetSDKReferenceFiles.cs index d1786ad7f1f..6099546f9af 100644 --- a/src/Tasks/GetSDKReferenceFiles.cs +++ b/src/Tasks/GetSDKReferenceFiles.cs @@ -567,7 +567,7 @@ private void GenerateOutputItems() /// private void GatherReferenceAssemblies(HashSet resolvedFiles, ITaskItem sdkReference, string path, SDKInfo info) { - if (info.DirectoryToFileList != null && info.DirectoryToFileList.TryGetValue(FileUtilities.EnsureNoTrailingSlash(path), out List referenceFiles) && referenceFiles != null) + if (info.DirectoryToFileList != null && info.DirectoryToFileList.TryGetValue(FrameworkFileUtilities.EnsureNoTrailingSlash(path), out List referenceFiles) && referenceFiles != null) { foreach (string file in referenceFiles) { @@ -619,7 +619,7 @@ private void GatherRedistFiles(HashSet resolvedRedistFiles, foreach (KeyValuePair> directoryToFileList in info.DirectoryToFileList) { // Add a trailing slash to ensure we don't match the start of a platform (e.g. ...\ARM matching ...\ARM64) - if (FileUtilities.EnsureTrailingSlash(directoryToFileList.Key).StartsWith(FileUtilities.EnsureTrailingSlash(redistFilePath), StringComparison.OrdinalIgnoreCase)) + if (FrameworkFileUtilities.EnsureTrailingSlash(directoryToFileList.Key).StartsWith(FrameworkFileUtilities.EnsureTrailingSlash(redistFilePath), StringComparison.OrdinalIgnoreCase)) { List redistFiles = directoryToFileList.Value; string targetPathRoot = sdkReference.GetMetadata("CopyRedistToSubDirectory"); diff --git a/src/Tasks/ListOperators/FindUnderPath.cs b/src/Tasks/ListOperators/FindUnderPath.cs index 836ec12b0f0..05646c412e4 100644 --- a/src/Tasks/ListOperators/FindUnderPath.cs +++ b/src/Tasks/ListOperators/FindUnderPath.cs @@ -59,8 +59,8 @@ public override bool Execute() { conePath = Strings.WeakIntern( - System.IO.Path.GetFullPath(FileUtilities.FixFilePath(Path.ItemSpec))); - conePath = FileUtilities.EnsureTrailingSlash(conePath); + System.IO.Path.GetFullPath(FrameworkFileUtilities.FixFilePath(Path.ItemSpec))); + conePath = FrameworkFileUtilities.EnsureTrailingSlash(conePath); } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { @@ -80,7 +80,7 @@ public override bool Execute() { fullPath = Strings.WeakIntern( - System.IO.Path.GetFullPath(FileUtilities.FixFilePath(item.ItemSpec))); + System.IO.Path.GetFullPath(FrameworkFileUtilities.FixFilePath(item.ItemSpec))); } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { diff --git a/src/Tasks/MakeDir.cs b/src/Tasks/MakeDir.cs index eb7d2ef3281..bf0c67ffa95 100644 --- a/src/Tasks/MakeDir.cs +++ b/src/Tasks/MakeDir.cs @@ -14,7 +14,8 @@ namespace Microsoft.Build.Tasks /// /// A task that creates a directory /// - public class MakeDir : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class MakeDir : TaskExtension, IIncrementalTask, IMultiThreadableTask { [Required] public ITaskItem[] Directories @@ -33,6 +34,9 @@ public ITaskItem[] Directories public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + private ITaskItem[] _directories; #region ITask Members @@ -53,24 +57,26 @@ public override bool Execute() // here we check for that case. if (directory.ItemSpec.Length > 0) { + AbsolutePath? absolutePath = null; try { - // For speed, eliminate duplicates caused by poor targets authoring + // For speed, eliminate duplicates caused by poor targets authoring, don't absolutize yet to save allocation if (!directoriesSet.Contains(directory.ItemSpec)) { + absolutePath = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(directory.ItemSpec)); // Only log a message if we actually need to create the folder - if (!FileUtilities.DirectoryExistsNoThrow(directory.ItemSpec)) + if (!FileUtilities.DirectoryExistsNoThrow(absolutePath)) { if (FailIfNotIncremental) { - Log.LogErrorFromResources("MakeDir.Comment", directory.ItemSpec); + Log.LogErrorFromResources("MakeDir.Comment", absolutePath.Value.OriginalValue); } else { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", directory.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", absolutePath.Value.OriginalValue); - Directory.CreateDirectory(FileUtilities.FixFilePath(directory.ItemSpec)); + Directory.CreateDirectory(absolutePath); } } @@ -79,7 +85,7 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("MakeDir.Error", directory.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("MakeDir.Error", absolutePath?.OriginalValue ?? directory.ItemSpec, e.Message); } // Add even on failure to avoid reattempting diff --git a/src/Tasks/RemoveDir.cs b/src/Tasks/RemoveDir.cs index 3e43ca69a80..5eeda7f53d4 100644 --- a/src/Tasks/RemoveDir.cs +++ b/src/Tasks/RemoveDir.cs @@ -16,13 +16,17 @@ namespace Microsoft.Build.Tasks /// /// Remove the specified directories. /// - public class RemoveDir : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class RemoveDir : TaskExtension, IIncrementalTask, IMultiThreadableTask { //----------------------------------------------------------------------------------- // Property: directory to remove //----------------------------------------------------------------------------------- private ITaskItem[] _directories; + /// + public TaskEnvironment TaskEnvironment { get; set; } + [Required] public ITaskItem[] Directories { @@ -61,31 +65,32 @@ public override bool Execute() continue; } - if (FileSystems.Default.DirectoryExists(directory.ItemSpec)) + AbsolutePath directoryPath = TaskEnvironment.GetAbsolutePath(directory.ItemSpec); + + if (FileSystems.Default.DirectoryExists(directoryPath)) { if (FailIfNotIncremental) { - Log.LogErrorFromResources("RemoveDir.Removing", directory.ItemSpec); + Log.LogErrorFromResources("RemoveDir.Removing", directoryPath.OriginalValue); continue; } // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directory.ItemSpec); - + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directoryPath.OriginalValue); // Try to remove the directory, this will not log unauthorized access errors since // we will attempt to remove read only attributes and try again. - bool currentSuccess = RemoveDirectory(directory, false, out bool unauthorizedAccess); + bool currentSuccess = RemoveDirectory(directoryPath, false, out bool unauthorizedAccess); // The first attempt failed, to we will remove readonly attributes and try again.. if (!currentSuccess && unauthorizedAccess) { // If the directory delete operation returns an unauthorized access exception // we need to attempt to remove the readonly attributes and try again. - currentSuccess = RemoveReadOnlyAttributeRecursively(new DirectoryInfo(directory.ItemSpec)); + currentSuccess = RemoveReadOnlyAttributeRecursively(new DirectoryInfo(directoryPath)); if (currentSuccess) { // Retry the remove directory operation, this time we want to log any errors - currentSuccess = RemoveDirectory(directory, true, out unauthorizedAccess); + currentSuccess = RemoveDirectory(directoryPath, true, out unauthorizedAccess); } } @@ -99,7 +104,7 @@ public override bool Execute() } else { - Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.SkippingNonexistentDirectory", directory.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.SkippingNonexistentDirectory", directoryPath.OriginalValue); // keep a running list of the directories that were actually removed // note that we include in this list directories that did not exist removedDirectoriesList.Add(new TaskItem(directory)); @@ -111,7 +116,7 @@ public override bool Execute() } // Core implementation of directory removal - private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out bool unauthorizedAccess) + private bool RemoveDirectory(AbsolutePath directoryPath, bool logUnauthorizedError, out bool unauthorizedAccess) { bool success = true; @@ -120,7 +125,7 @@ private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out try { // Try to delete the directory - Directory.Delete(directory.ItemSpec, true); + Directory.Delete(directoryPath, true); } catch (UnauthorizedAccessException e) { @@ -128,13 +133,13 @@ private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out // Log the fact that there was a problem only if we have been asked to. if (logUnauthorizedError) { - Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory, e.Message); + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directoryPath.OriginalValue, e.Message); } unauthorizedAccess = true; } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directoryPath.OriginalValue, e.Message); success = false; } diff --git a/src/Tasks/ResGenDependencies.cs b/src/Tasks/ResGenDependencies.cs index 72713bb21ce..a593be6cffe 100644 --- a/src/Tasks/ResGenDependencies.cs +++ b/src/Tasks/ResGenDependencies.cs @@ -8,13 +8,14 @@ #if FEATURE_RESXREADER_LIVEDESERIALIZATION using System.Collections; using System.Resources; +using Microsoft.Build.Shared; #endif using System.Xml; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.ResourceHandling; using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; #nullable disable @@ -370,7 +371,7 @@ internal bool AllOutputFilesAreUpToDate() Debug.Assert(outputFiles != null, "OutputFiles hasn't been set"); foreach (string outputFileName in outputFiles) { - var outputFile = new FileInfo(FileUtilities.FixFilePath(outputFileName)); + var outputFile = new FileInfo(FrameworkFileUtilities.FixFilePath(outputFileName)); if (!outputFile.Exists || outputFile.LastWriteTime < LastModified) { return false; diff --git a/src/Tasks/ResolveSDKReference.cs b/src/Tasks/ResolveSDKReference.cs index 9d2c288a93e..7b759a91a79 100644 --- a/src/Tasks/ResolveSDKReference.cs +++ b/src/Tasks/ResolveSDKReference.cs @@ -916,7 +916,7 @@ public void Resolve(Dictionary sdks, string targetConfigurati _prefer32BitFromProject = prefer32Bit; // There must be a trailing slash or else the ExpandSDKReferenceAssemblies will not work. - ResolvedPath = FileUtilities.EnsureTrailingSlash(sdk.ItemSpec); + ResolvedPath = FrameworkFileUtilities.EnsureTrailingSlash(sdk.ItemSpec); System.Version.TryParse(sdk.GetMetadata(SDKPlatformVersion), out Version targetPlatformVersionFromItem); diff --git a/src/Tasks/ResourceHandling/MSBuildResXReader.cs b/src/Tasks/ResourceHandling/MSBuildResXReader.cs index 9186ac7b382..90e0f34cbad 100644 --- a/src/Tasks/ResourceHandling/MSBuildResXReader.cs +++ b/src/Tasks/ResourceHandling/MSBuildResXReader.cs @@ -8,6 +8,7 @@ using System.Text; using System.Xml; using System.Xml.Linq; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -228,7 +229,7 @@ private static void AddLinkedResource(string resxFilename, bool pathsRelativeToB { string[] fileRefInfo = ParseResxFileRefString(value); - string fileName = FileUtilities.FixFilePath(fileRefInfo[0]); + string fileName = FrameworkFileUtilities.FixFilePath(fileRefInfo[0]); string fileRefType = fileRefInfo[1]; if (pathsRelativeToBasePath) diff --git a/src/Tasks/System.Resources.Extensions.pkgdef b/src/Tasks/System.Resources.Extensions.pkgdef index a90ae6d001b..eff98829712 100644 --- a/src/Tasks/System.Resources.Extensions.pkgdef +++ b/src/Tasks/System.Resources.Extensions.pkgdef @@ -3,5 +3,5 @@ "codeBase"="$BaseInstallDir$\MSBuild\Current\Bin\System.Resources.Extensions.dll" "publicKeyToken"="cc7b13ffcd2ddd51" "culture"="neutral" -"oldVersion"="0.0.0.0-9.0.0.11" -"newVersion"="9.0.0.11" +"oldVersion"="0.0.0.0-10.0.0.1" +"newVersion"="10.0.0.1" diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs index 6b66ec1e769..8f442777033 100644 --- a/src/Tasks/Touch.cs +++ b/src/Tasks/Touch.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// This class defines the touch task. /// - public class Touch : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class Touch : TaskExtension, IIncrementalTask, IMultiThreadableTask { private MessageImportance messageImportance; @@ -48,6 +49,9 @@ public class Touch : TaskExtension, IIncrementalTask [Output] public ITaskItem[] TouchedFiles { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Importance: high, normal, low (default normal) /// @@ -91,7 +95,8 @@ internal bool ExecuteImpl( foreach (ITaskItem file in Files) { - string path = FileUtilities.FixFilePath(file.ItemSpec); + AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); + // For speed, eliminate duplicates caused by poor targets authoring if (touchedFilesSet.Contains(path)) { @@ -164,7 +169,7 @@ public override bool Execute() /// /// "true" if the file was created. private bool CreateFile( - string file, + AbsolutePath file, FileCreate fileCreate) { try @@ -175,7 +180,7 @@ private bool CreateFile( } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file, e.Message); + Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file.OriginalValue, e.Message); return false; } @@ -187,7 +192,7 @@ private bool CreateFile( /// /// "True" if the file was touched. private bool TouchFile( - string file, + AbsolutePath file, DateTime dt, FileExists fileExists, FileCreate fileCreate, @@ -203,11 +208,11 @@ private bool TouchFile( { if (FailIfNotIncremental) { - Log.LogWarningFromResources("Touch.CreatingFile", file, "AlwaysCreate"); + Log.LogWarningFromResources("Touch.CreatingFile", file.OriginalValue, "AlwaysCreate"); } else { - Log.LogMessageFromResources(messageImportance, "Touch.CreatingFile", file, "AlwaysCreate"); + Log.LogMessageFromResources(messageImportance, "Touch.CreatingFile", file.OriginalValue, "AlwaysCreate"); } if (!CreateFile(file, fileCreate)) @@ -217,18 +222,18 @@ private bool TouchFile( } else { - Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file); + Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.OriginalValue); return false; } } if (FailIfNotIncremental) { - Log.LogWarningFromResources("Touch.Touching", file); + Log.LogWarningFromResources("Touch.Touching", file.OriginalValue); } else { - Log.LogMessageFromResources(messageImportance, "Touch.Touching", file); + Log.LogMessageFromResources(messageImportance, "Touch.Touching", file.OriginalValue); } // If the file is read only then we must either issue an error, or, if the user so @@ -248,7 +253,7 @@ private bool TouchFile( catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(file); - Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file.OriginalValue, e.Message, lockedFileMessage); return false; } } @@ -264,7 +269,7 @@ private bool TouchFile( catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(file); - Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file.OriginalValue, e.Message, lockedFileMessage); return false; } finally @@ -279,7 +284,7 @@ private bool TouchFile( } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file, e.Message); + Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file.OriginalValue, e.Message); retVal = false; } } diff --git a/src/Tasks/Unzip.cs b/src/Tasks/Unzip.cs index f04f84d3eb3..6492508e130 100644 --- a/src/Tasks/Unzip.cs +++ b/src/Tasks/Unzip.cs @@ -160,7 +160,7 @@ public override bool Execute() /// The to extract files to. private void Extract(ZipArchive sourceArchive, DirectoryInfo destinationDirectory) { - string fullDestinationDirectoryPath = Path.GetFullPath(FileUtilities.EnsureTrailingSlash(destinationDirectory.FullName)); + string fullDestinationDirectoryPath = Path.GetFullPath(FrameworkFileUtilities.EnsureTrailingSlash(destinationDirectory.FullName)); foreach (ZipArchiveEntry zipArchiveEntry in sourceArchive.Entries.TakeWhile(i => !_cancellationToken.IsCancellationRequested)) { diff --git a/src/Tasks/Warning.cs b/src/Tasks/Warning.cs index 49ba1e927ff..bb5a364d185 100644 --- a/src/Tasks/Warning.cs +++ b/src/Tasks/Warning.cs @@ -3,9 +3,7 @@ #nullable disable -#if NET using System.Diagnostics.CodeAnalysis; -#endif using Microsoft.Build.Framework; namespace Microsoft.Build.Tasks diff --git a/src/Utilities.UnitTests/CommandLineBuilder_Tests.cs b/src/Utilities.UnitTests/CommandLineBuilder_Tests.cs index 6946d9e2323..7f07523a3dc 100644 --- a/src/Utilities.UnitTests/CommandLineBuilder_Tests.cs +++ b/src/Utilities.UnitTests/CommandLineBuilder_Tests.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Threading; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; using Shouldly; @@ -107,11 +108,11 @@ public void AppendLiteralSwitchWithSpacesInParameter() public void AppendTwoStringsEnsureNoSpace() { CommandLineBuilder c = new CommandLineBuilder(); - c.AppendFileNamesIfNotNull(new[] { "Form1.resx", FileUtilities.FixFilePath("built\\Form1.resources") }, ","); + c.AppendFileNamesIfNotNull(new[] { "Form1.resx", FrameworkFileUtilities.FixFilePath("built\\Form1.resources") }, ","); // There shouldn't be a space before or after the comma // Tools like resgen require comma-delimited lists to be bumped up next to each other. - c.ShouldBe(FileUtilities.FixFilePath(@"Form1.resx,built\Form1.resources")); + c.ShouldBe(FrameworkFileUtilities.FixFilePath(@"Form1.resx,built\Form1.resources")); } /* diff --git a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs index a14605728ea..4104fc438d5 100644 --- a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs +++ b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs @@ -2204,7 +2204,7 @@ public void GetPathToStandardLibraries64Bit40() } string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", string.Empty, "x86"); - string dotNet40Path = FileUtilities.EnsureNoTrailingSlash(referencePaths[0]); + string dotNet40Path = FrameworkFileUtilities.EnsureNoTrailingSlash(referencePaths[0]); pathToFramework.ShouldBe(dotNet40Path, StringCompareShould.IgnoreCase); pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", string.Empty, "x64"); @@ -2287,7 +2287,7 @@ public void GetPathToStandardLibraries32Bit40() } string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", string.Empty, "x86"); - string dotNet40Path = FileUtilities.EnsureNoTrailingSlash(referencePaths[0]); + string dotNet40Path = FrameworkFileUtilities.EnsureNoTrailingSlash(referencePaths[0]); pathToFramework.ShouldBe(dotNet40Path, StringCompareShould.IgnoreCase); pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", string.Empty, "x64"); diff --git a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs index d41e3cbf049..f5cdd7f1e79 100644 --- a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs +++ b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs @@ -338,8 +338,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // Should track directories when '/e' is passed - FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -351,8 +351,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -364,8 +364,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // With neither '/a' nor '/e', should not do any directory tracking whatsoever - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -377,7 +377,7 @@ static void Main(string[] args) Assert.Equal(0, exit); // Should track directories when '/e' is passed - FileTrackerTestHelper.AssertFoundStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -389,7 +389,7 @@ static void Main(string[] args) Assert.Equal(0, exit); // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. - FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -401,7 +401,7 @@ static void Main(string[] args) Assert.Equal(0, exit); // With neither '/a' nor '/e', should not do any directory tracking whatsoever - FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); } finally { @@ -900,12 +900,12 @@ public void FileTrackerFileIsExcludedFromDependencies() // The short path to temp string tempShortPath = NativeMethodsShared.IsUnixLike ? tempPath - : FileUtilities.EnsureTrailingSlash( + : FrameworkFileUtilities.EnsureTrailingSlash( NativeMethodsShared.GetShortFilePath(tempPath).ToUpperInvariant()); // The long path to temp string tempLongPath = NativeMethodsShared.IsUnixLike ? tempPath - : FileUtilities.EnsureTrailingSlash( + : FrameworkFileUtilities.EnsureTrailingSlash( NativeMethodsShared.GetLongFilePath(tempPath).ToUpperInvariant()); // We don't want to be including these as dependencies or outputs: diff --git a/src/Utilities/CommandLineBuilder.cs b/src/Utilities/CommandLineBuilder.cs index 07ac12544e9..481dc35c62e 100644 --- a/src/Utilities/CommandLineBuilder.cs +++ b/src/Utilities/CommandLineBuilder.cs @@ -336,7 +336,7 @@ protected void AppendFileNameWithQuoting(string fileName) // their own quotes. Quotes are illegal. VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileName); - fileName = FileUtilities.FixFilePath(fileName); + fileName = FrameworkFileUtilities.FixFilePath(fileName); if (fileName.Length != 0 && fileName[0] == '-') { AppendTextWithQuoting("." + Path.DirectorySeparatorChar + fileName); diff --git a/src/Utilities/TargetPlatformSDK.cs b/src/Utilities/TargetPlatformSDK.cs index 64ef39b0ed5..ea7b3408cd7 100644 --- a/src/Utilities/TargetPlatformSDK.cs +++ b/src/Utilities/TargetPlatformSDK.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -104,7 +105,7 @@ public Version MinOSVersion public string Path { get => _path; - set => _path = value != null ? FileUtilities.EnsureTrailingSlash(value) : null; + set => _path = value != null ? FrameworkFileUtilities.EnsureTrailingSlash(value) : null; } /// diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs index 506e233a0a6..2fa8a3789a1 100644 --- a/src/Utilities/TaskItem.cs +++ b/src/Utilities/TaskItem.cs @@ -103,7 +103,7 @@ public TaskItem( { ErrorUtilities.VerifyThrowArgumentNull(itemSpec); - _itemSpec = treatAsFilePath ? FileUtilities.FixFilePath(itemSpec) : itemSpec; + _itemSpec = treatAsFilePath ? FrameworkFileUtilities.FixFilePath(itemSpec) : itemSpec; } /// @@ -185,7 +185,7 @@ public string ItemSpec { ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ItemSpec)); - _itemSpec = FileUtilities.FixFilePath(value); + _itemSpec = FrameworkFileUtilities.FixFilePath(value); _fullPath = null; } } @@ -204,7 +204,7 @@ string ITaskItem2.EvaluatedIncludeEscaped set { - _itemSpec = FileUtilities.FixFilePath(value); + _itemSpec = FrameworkFileUtilities.FixFilePath(value); _fullPath = null; } } diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs index 673ba0eb6b0..9e7ebbf82cd 100644 --- a/src/Utilities/ToolLocationHelper.cs +++ b/src/Utilities/ToolLocationHelper.cs @@ -9,6 +9,7 @@ using System.Runtime.Versioning; using System.Text; using System.Xml; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyFoldersFromConfig; @@ -663,7 +664,7 @@ public static IList GetSDKReferenceFolders(string sdkRoot, string target string legacyWindowsMetadataLocation = Path.Combine(sdkRoot, "Windows Metadata"); if (FileUtilities.DirectoryExistsNoThrow(legacyWindowsMetadataLocation)) { - legacyWindowsMetadataLocation = FileUtilities.EnsureTrailingSlash(legacyWindowsMetadataLocation); + legacyWindowsMetadataLocation = FrameworkFileUtilities.EnsureTrailingSlash(legacyWindowsMetadataLocation); referenceDirectories.Add(legacyWindowsMetadataLocation); } @@ -1766,7 +1767,7 @@ public static string GetPathToStandardLibraries(string targetFrameworkIdentifier { // We found the framework reference assembly directory with mscorlib in it // that's our standard lib path, so return it, with no trailing slash. - return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); + return FrameworkFileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); } } @@ -1840,7 +1841,7 @@ public static string GetPathToStandardLibraries(string targetFrameworkIdentifier { // We found the framework reference assembly directory with mscorlib in it // that's our standard lib path, so return it, with no trailing slash. - return FileUtilities.EnsureNoTrailingSlash(legacyMsCorlib20Path); + return FrameworkFileUtilities.EnsureNoTrailingSlash(legacyMsCorlib20Path); } // If for some reason the 2.0 framework is not installed in its default location then maybe someone is using the ".net 4.0" reference assembly @@ -1857,7 +1858,7 @@ public static string GetPathToStandardLibraries(string targetFrameworkIdentifier { // We found the framework reference assembly directory with mscorlib in it // that's our standard lib path, so return it, with no trailing slash. - return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); + return FrameworkFileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); } } @@ -2426,7 +2427,7 @@ private static void AddSDKPath(string sdkRoot, string contentFolderName, string if (FileUtilities.DirectoryExistsNoThrow(referenceAssemblyPath)) { - referenceAssemblyPath = FileUtilities.EnsureTrailingSlash(referenceAssemblyPath); + referenceAssemblyPath = FrameworkFileUtilities.EnsureTrailingSlash(referenceAssemblyPath); contentDirectories.Add(referenceAssemblyPath); } } @@ -2552,7 +2553,7 @@ internal static void GatherExtensionSDKs(DirectoryInfo extensionSdksDirectory, T string pathToSDKManifest = Path.Combine(sdkVersionDirectory.FullName, "SDKManifest.xml"); if (FileUtilities.FileExistsNoThrow(pathToSDKManifest)) { - targetPlatformSDK.ExtensionSDKs.Add(SDKKey, FileUtilities.EnsureTrailingSlash(sdkVersionDirectory.FullName)); + targetPlatformSDK.ExtensionSDKs.Add(SDKKey, FrameworkFileUtilities.EnsureTrailingSlash(sdkVersionDirectory.FullName)); } else { @@ -2825,7 +2826,7 @@ internal static void GatherSDKsFromRegistryImpl(Dictionary - private static readonly string s_tempPath = FileUtilities.EnsureTrailingSlash(Path.GetTempPath()); + private static readonly string s_tempPath = FrameworkFileUtilities.EnsureTrailingSlash(Path.GetTempPath()); // The short path to temp - private static readonly string s_tempShortPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetShortFilePath(s_tempPath).ToUpperInvariant()); + private static readonly string s_tempShortPath = FrameworkFileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetShortFilePath(s_tempPath).ToUpperInvariant()); // The long path to temp - private static readonly string s_tempLongPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetLongFilePath(s_tempPath).ToUpperInvariant()); + private static readonly string s_tempLongPath = FrameworkFileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetLongFilePath(s_tempPath).ToUpperInvariant()); // The path to ApplicationData (is equal to %USERPROFILE%\Application Data folder in Windows XP and %USERPROFILE%\AppData\Roaming in Vista and later) - private static readonly string s_applicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).ToUpperInvariant()); + private static readonly string s_applicationDataPath = FrameworkFileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).ToUpperInvariant()); // The path to LocalApplicationData (is equal to %USERPROFILE%\Local Settings\Application Data folder in Windows XP and %USERPROFILE%\AppData\Local in Vista and later). - private static readonly string s_localApplicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).ToUpperInvariant()); + private static readonly string s_localApplicationDataPath = FrameworkFileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).ToUpperInvariant()); // The path to the LocalLow folder. In Vista and later, user application data is organized across %USERPROFILE%\AppData\LocalLow, %USERPROFILE%\AppData\Local (%LOCALAPPDATA%) // and %USERPROFILE%\AppData\Roaming (%APPDATA%). The LocalLow folder is not present in XP. - private static readonly string s_localLowApplicationDataPath = FileUtilities.EnsureTrailingSlash(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\LocalLow").ToUpperInvariant()); + private static readonly string s_localLowApplicationDataPath = FrameworkFileUtilities.EnsureTrailingSlash(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\LocalLow").ToUpperInvariant()); // The path to the common Application Data, which is also used by some programs (e.g. antivirus) that we wish to ignore. // Is equal to C:\Documents and Settings\All Users\Application Data on XP, and C:\ProgramData on Vista+. @@ -127,18 +127,18 @@ private static List InitializeCommonApplicationDataPaths() { List commonApplicationDataPaths = new(); - string defaultCommonApplicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData).ToUpperInvariant()); + string defaultCommonApplicationDataPath = FrameworkFileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData).ToUpperInvariant()); commonApplicationDataPaths.Add(defaultCommonApplicationDataPath); string defaultRootDirectory = Path.GetPathRoot(defaultCommonApplicationDataPath); - string alternativeCommonApplicationDataPath1 = FileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Documents and Settings\All Users\Application Data").ToUpperInvariant()); + string alternativeCommonApplicationDataPath1 = FrameworkFileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Documents and Settings\All Users\Application Data").ToUpperInvariant()); if (!alternativeCommonApplicationDataPath1.Equals(defaultCommonApplicationDataPath, StringComparison.Ordinal)) { commonApplicationDataPaths.Add(alternativeCommonApplicationDataPath1); } - string alternativeCommonApplicationDataPath2 = FileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Users\All Users\Application Data").ToUpperInvariant()); + string alternativeCommonApplicationDataPath2 = FrameworkFileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Users\All Users\Application Data").ToUpperInvariant()); if (!alternativeCommonApplicationDataPath2.Equals(defaultCommonApplicationDataPath, StringComparison.Ordinal)) { @@ -281,7 +281,7 @@ public static bool FileIsUnderPath(string fileName, string path) // Ensure that the path has a trailing slash that we are checking under // By default the paths that we check for most often will have, so this will // return fast and not allocate memory in the process - return FileIsUnderNormalizedPath(fileName, FileUtilities.EnsureTrailingSlash(path)); + return FileIsUnderNormalizedPath(fileName, FrameworkFileUtilities.EnsureTrailingSlash(path)); } internal static bool FileIsUnderNormalizedPath(string fileName, string path) @@ -615,7 +615,7 @@ public static string TrackerResponseFileArguments(string dllName, string interme { intermediateDirectory = FileUtilities.NormalizePath(intermediateDirectory); // If the intermediate directory ends up with a trailing slash, then be rid of it! - if (FileUtilities.EndsWithSlash(intermediateDirectory)) + if (FrameworkFileUtilities.EndsWithSlash(intermediateDirectory)) { intermediateDirectory = Path.GetDirectoryName(intermediateDirectory); } diff --git a/src/Utilities/TrackedDependencies/FlatTrackingData.cs b/src/Utilities/TrackedDependencies/FlatTrackingData.cs index 633dc4c02ac..e12cee660ff 100644 --- a/src/Utilities/TrackedDependencies/FlatTrackingData.cs +++ b/src/Utilities/TrackedDependencies/FlatTrackingData.cs @@ -323,7 +323,7 @@ private void InternalConstruct(ITask ownerTask, ITaskItem[] tlogFilesLocal, ITas // our "starts with" comparison doesn't pick up incomplete matches, such as C:\Foo matching C:\FooFile.txt foreach (string excludePath in excludedInputPaths) { - string fullexcludePath = FileUtilities.EnsureTrailingSlash(FileUtilities.NormalizePath(excludePath)).ToUpperInvariant(); + string fullexcludePath = FrameworkFileUtilities.EnsureTrailingSlash(FileUtilities.NormalizePath(excludePath)).ToUpperInvariant(); _excludedInputPaths.Add(fullexcludePath); } } diff --git a/template_feed/content/Microsoft.CheckTemplate/.template.config/template.json b/template_feed/content/Microsoft.CheckTemplate/.template.config/template.json index dd3df2a12b5..571de2cb85a 100644 --- a/template_feed/content/Microsoft.CheckTemplate/.template.config/template.json +++ b/template_feed/content/Microsoft.CheckTemplate/.template.config/template.json @@ -27,7 +27,7 @@ "type": "parameter", "description": "Overrides the default Microsoft.Build version where check's interfaces are placed", "datatype": "text", - "defaultValue": "18.3.0", + "defaultValue": "18.4.0", "replaces": "1.0.0-MicrosoftBuildPackageVersion", "displayName": "Microsoft.Build default package version override" }