diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3fe0b198957f..5b78ef3cba1f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,7 +25,8 @@ Testing: - `dotnet exec artifacts/bin/redist/Debug/dotnet.Tests.dll -method "*ItShowsTheAppropriateMessageToTheUser*"` - To test CLI command changes: - Build the redist SDK: `./build.sh` from repo root - - Create a dogfood environment: `source eng/dogfood.sh` + - IMPORTANT: specify `/p:SkipUsingCrossgen=true` and `/p:SkipBuildingInstallers=true` to drastically speed up the build time. + - Create a dogfood environment: `source eng/dogfood.sh` - Test commands in the dogfood shell (e.g., `dnx --help`, `dotnet tool install --help`) - The dogfood script sets up PATH and environment to use the newly built SDK @@ -43,3 +44,9 @@ External Dependencies: - Changes that require modifications to the dotnet/templating repository (Microsoft.TemplateEngine packages) should be made directly in that repository, not worked around in this repo. - The dotnet/templating repository owns the TemplateEngine.Edge, TemplateEngine.Abstractions, and related packages. - If a change requires updates to template engine behavior or formatting (e.g., DisplayName properties), file an issue in dotnet/templating and make the changes there rather than adding workarounds in this SDK repository. + +Package Versioning: +- Package versions should be managed through version property files, not hard-coded in Directory.Packages.props. +- For packages from .NET repos (dotnet/runtime, dotnet/roslyn, etc.): Add version to eng/Version.Details.xml and eng/Version.Details.props. These are auto-updated by Maestro dependency flow. +- For packages from other sources: Add version to eng/Versions.props for manual version management. +- In Directory.Packages.props, always reference the version property (e.g., $(MicrosoftCodeAnalysisBannedApiAnalyzersPackageVersion)) instead of hard-coding version numbers. diff --git a/BannedSymbols.txt b/BannedSymbols.txt new file mode 100644 index 000000000000..203ebb7eba90 --- /dev/null +++ b/BannedSymbols.txt @@ -0,0 +1,9 @@ +# Ban direct usage of MSBuild APIs to ensure proper telemetry integration and evaluation caching +# Use DotNetProjectEvaluator and DotNetProjectBuilder wrapper types instead + +# Ban direct project creation/manipulation +T:Microsoft.Build.Execution.ProjectInstance;Use DotNetProjectEvaluator.LoadProject() to get a DotNetProject instead +T:Microsoft.Build.Evaluation.Project;Use DotNetProjectEvaluator.LoadProject() to get a DotNetProject instead + +# Ban direct ProjectCollection creation - use DotNetProjectEvaluatorFactory instead +T:Microsoft.Build.Evaluation.ProjectCollection;Use DotNetProjectEvaluatorFactory to get a DotNetProjectEvaluator instead diff --git a/Directory.Packages.props b/Directory.Packages.props index 1a83625bdc29..530715c5b2ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + diff --git a/documentation/project-docs/msbuild-api-usage-unification.md b/documentation/project-docs/msbuild-api-usage-unification.md new file mode 100644 index 000000000000..48105a6f7778 --- /dev/null +++ b/documentation/project-docs/msbuild-api-usage-unification.md @@ -0,0 +1,401 @@ +# MSBuild API Usage Unification Plan + +## Overview + +This document outlines the plan to unify and improve MSBuild API usage across the dotnet-sdk codebase by replacing direct MSBuild API access with purpose-built wrapper types that enforce best practices, telemetry integration, and evaluation caching. + +## Current State Analysis + +### MSBuild API Usage Patterns + +Based on comprehensive codebase analysis, MSBuild APIs are used in the following patterns: + +#### 1. **Project Property Evaluation** (~60% of usage) +Reading project properties and metadata without building: +- Target framework detection (`TargetFramework`, `TargetFrameworks`) +- Configuration analysis (`Configuration`, `Platform`, `OutputType`) +- Path resolution (`OutputPath`, `ProjectAssetsFile`, `MSBuildProjectFullPath`) +- Feature detection (container support, workload requirements, package management) + +#### 2. **Project Item Collection Analysis** (~20% of usage) +Inspecting project items and references: +- Dependency analysis (`PackageReference`, `ProjectReference`) +- Asset discovery (`Content`, `EmbeddedResource`, `Compile`) +- Container configuration (`ContainerLabel`, `ContainerEnvironmentVariable`) +- Workload requirements analysis + +#### 3. **Build Target Execution** (~10% of usage) +Running specific MSBuild targets with telemetry: +- Workload analysis (`_GetRequiredWorkloads`) +- Dependency graph generation (`GenerateRestoreGraphFile`) +- Run preparation (`_GetRunCommand`) +- Container operations + +#### 4. **Solution and Multi-Project Operations** (~5% of usage) +Managing collections of projects: +- Solution analysis and cross-project operations +- Reference management (add/remove project references) +- Bulk operations across multiple projects + +#### 5. **Command-Specific Scenarios** (~5% of usage) +- `dotnet run`: Project executability checks and launch configuration +- Package commands: PackageReference analysis and central package management +- Reference commands: ProjectReference management and validation +- Workload commands: Project requirement analysis + +### Current Problems + +#### 1. **Evaluation Caching Anti-Patterns** +- **Direct ProjectInstance Creation**: Many locations use `new ProjectInstance(projectFile, globalProperties, null)` instead of leveraging ProjectCollection caching +- **Short-lived ProjectCollection Pattern**: Most commands create new ProjectCollection per operation with immediate disposal +- **Inconsistent Global Properties**: Multiple construction points create variations that reduce cache hit rates + +#### 2. **Telemetry Integration Issues** +- Manual telemetry logger setup required at each usage site +- Risk of forgetting telemetry integration in new code +- Complex distributed logging setup for build scenarios + +#### 3. **Resource Management Complexity** +- ProjectCollection disposal requirements not always properly handled +- Memory leaks possible with improper lifecycle management +- No centralized resource management strategy + +#### 4. **API Misuse Potential** +- Direct access to MSBuild APIs allows bypassing best practices +- No enforcement of telemetry integration +- Inconsistent error handling patterns + +## Global Properties Analysis + +### Common Global Properties Used + +**Core Properties (from MSBuildPropertyNames):** +- `Configuration` - Release/Debug configuration +- `TargetFramework` - Specific target framework for evaluation +- `TargetFrameworks` - Multi-targeting scenarios +- `PublishRelease`/`PackRelease` - Release optimization properties + +**Restore-Specific Properties:** +```csharp +{ "EnableDefaultCompileItems", "false" }, +{ "EnableDefaultEmbeddedResourceItems", "false" }, +{ "EnableDefaultNoneItems", "false" }, +{ "MSBuildRestoreSessionId", Guid.NewGuid().ToString("D") }, +{ "MSBuildIsRestoring", "true" } +``` + +**Runtime Properties:** +- `DOTNET_HOST_PATH` - Always set to current host path +- User-specified properties from command line (`-p:Property=Value`) + +**Virtual Project Properties:** +```csharp +{ "_BuildNonexistentProjectsByDefault", "true" }, +{ "RestoreUseSkipNonexistentTargets", "false" }, +{ "ProvideCommandLineArgs", "true" } +``` + +### Global Properties Construction Patterns + +- **`CommonRunHelpers.GetGlobalPropertiesFromArgs`** - Most common pattern for run/test/build commands +- **`ReleasePropertyProjectLocator.InjectTargetFrameworkIntoGlobalProperties`** - Framework option handling +- **Command-specific constructors** - Various specialized property sets + +## Existing Caching Mechanisms + +### VirtualProjectBuildingCommand (Advanced Example) +- JSON-based cache with `CacheContext.GetCacheKey()` +- Cache keys include: global properties, SDK version, runtime version, directives, implicit build files +- File-based cache with timestamp validation +- Sophisticated invalidation based on file changes and version mismatches + +### MSBuildEvaluator (Simple Example) +- In-memory caching based on `ProjectCollection` instance +- Single command execution lifetime scope +- Used for template engine evaluations + +### Current Limitations +- Most evaluation scenarios don't benefit from caching +- ProjectCollection instances are short-lived +- No sharing of evaluation results across similar operations + +## Proposed Architecture + +### Wrapper Type Design + +#### 1. **DotNetProjectEvaluator** (Evaluation-Only Scenarios) +**Purpose**: Manages project evaluation with caching and consistent global properties + +```csharp +public sealed class DotNetProjectEvaluator : IDisposable +{ + // Configuration + public DotNetProjectEvaluator(IDictionary? globalProperties = null, + IEnumerable? loggers = null); + + // Core evaluation methods + public DotNetProject LoadProject(string projectPath); + public DotNetProject LoadProject(string projectPath, IDictionary? additionalGlobalProperties); + + // Batch operations for solutions + public IEnumerable LoadProjects(IEnumerable projectPaths); + + // Resource management + public void Dispose(); +} +``` + +**Features**: +- Manages ProjectCollection lifecycle with proper disposal +- Automatic telemetry logger integration +- Evaluation result caching within evaluator instance +- Consistent global property management +- Thread-safe for concurrent evaluations + +#### 2. **DotNetProject** (Project Wrapper) +**Purpose**: Provides typed access to project properties and items + +```csharp +public sealed class DotNetProject +{ + // Basic properties + public string FullPath { get; } + public string Directory { get; } + + // Strongly-typed common properties + public string? TargetFramework => GetPropertyValue("TargetFramework"); + public string[] TargetFrameworks => GetPropertyValue("TargetFrameworks")?.Split(';') ?? Array.Empty(); + public string Configuration => GetPropertyValue("Configuration") ?? "Debug"; + public string Platform => GetPropertyValue("Platform") ?? "AnyCPU"; + public string OutputType => GetPropertyValue("OutputType") ?? ""; + public string? OutputPath => GetPropertyValue("OutputPath"); + + // Generic property access + public string? GetPropertyValue(string propertyName); + public IEnumerable GetPropertyValues(string propertyName); // For multi-value properties + + // Item access + public IEnumerable GetItems(string itemType); + public IEnumerable GetItems(string itemType, Func predicate); + + // Convenience methods + public IEnumerable GetConfigurations(); + public IEnumerable GetPlatforms(); + public string GetProjectId(); + public string? GetDefaultProjectTypeGuid(); + + // No public constructor - created by DotNetProjectEvaluator +} +``` + +#### 3. **DotNetProjectBuilder** (Build Execution) +**Purpose**: Handles target execution with telemetry integration + +```csharp +public sealed class DotNetProjectBuilder +{ + public DotNetProjectBuilder(DotNetProject project, ILogger? telemetryCentralLogger = null); + + // Build operations + public BuildResult Build(params string[] targets); + public BuildResult Build(string[] targets, IEnumerable? additionalLoggers); + public BuildResult Build(string[] targets, out IDictionary targetOutputs); + + // Advanced build with custom remote loggers + public BuildResult Build(string[] targets, + IEnumerable? loggers, + IEnumerable? remoteLoggers, + out IDictionary targetOutputs); +} + +public record BuildResult(bool Success, IDictionary? TargetOutputs = null); +``` + +#### 4. **DotNetProjectItem** (Item Wrapper) +**Purpose**: Provides typed access to project items + +```csharp +public sealed class DotNetProjectItem +{ + public string ItemType { get; } + public string EvaluatedInclude { get; } + public string UnevaluatedInclude { get; } + + public string? GetMetadataValue(string metadataName); + public IEnumerable GetMetadataNames(); + public IDictionary GetMetadata(); +} +``` + +### Factory and Configuration + +#### DotNetProjectEvaluatorFactory +```csharp +public static class DotNetProjectEvaluatorFactory +{ + // Standard configurations + public static DotNetProjectEvaluator CreateForCommand(MSBuildArgs? args = null); + public static DotNetProjectEvaluator CreateForRestore(); + public static DotNetProjectEvaluator CreateForWorkloadAnalysis(); + + // Custom configuration + public static DotNetProjectEvaluator Create(IDictionary? globalProperties = null, + IEnumerable? loggers = null); +} +``` + +### Integration with Existing Telemetry + +The wrapper types will integrate with the existing telemetry infrastructure from `ProjectInstanceExtensions.cs`: + +- **Central Logger Creation**: `CreateTelemetryCentralLogger()` → Used internally by wrappers +- **Distributed Logging**: `CreateTelemetryForwardingLoggerRecords()` → Used by `DotNetProjectBuilder` +- **Logger Management**: `CreateLoggersWithTelemetry()` → Used by `DotNetProjectEvaluator` + +### BannedApiAnalyzer Integration + +Update `BannedSymbols.txt` to restrict direct MSBuild API usage: + +``` +# Direct ProjectInstance creation - use DotNetProjectEvaluator instead +T:Microsoft.Build.Execution.ProjectInstance.#ctor(System.String,System.Collections.Generic.IDictionary{System.String,System.String},System.String)~Use DotNetProjectEvaluator.LoadProject instead + +# Direct ProjectCollection creation without telemetry - use DotNetProjectEvaluatorFactory +T:Microsoft.Build.Evaluation.ProjectCollection.#ctor()~Use DotNetProjectEvaluatorFactory.Create instead +T:Microsoft.Build.Evaluation.ProjectCollection.#ctor(System.Collections.Generic.IDictionary{System.String,System.String})~Use DotNetProjectEvaluatorFactory.Create instead + +# Direct Project.Build calls - use DotNetProjectBuilder +M:Microsoft.Build.Execution.ProjectInstance.Build()~Use DotNetProjectBuilder.Build instead +M:Microsoft.Build.Execution.ProjectInstance.Build(System.String[])~Use DotNetProjectBuilder.Build instead +M:Microsoft.Build.Execution.ProjectInstance.Build(System.String[],System.Collections.Generic.IEnumerable{Microsoft.Build.Framework.ILogger})~Use DotNetProjectBuilder.Build instead + +# Direct Evaluation.Project usage - use DotNetProjectEvaluator +T:Microsoft.Build.Evaluation.Project~Use DotNetProjectEvaluator and DotNetProject instead +``` + +## Migration Strategy + +### Phase 1: Create Wrapper Infrastructure +1. Implement core wrapper types (`DotNetProjectEvaluator`, `DotNetProject`, `DotNetProjectBuilder`, `DotNetProjectItem`) +2. Create factory class with standard configurations +3. Add comprehensive unit tests +4. Migrate telemetry integration from `ProjectInstanceExtensions.cs` + +### Phase 2: Update High-Impact Usage Sites +1. **MSBuildEvaluator** - Replace with `DotNetProjectEvaluator` +2. **ReleasePropertyProjectLocator** - Use cached evaluation +3. **CommonRunHelpers** - Standardize global property construction +4. **Solution processing** - Leverage batch evaluation capabilities + +### Phase 3: Command Integration +1. **Run Command** - Update project analysis and launch preparation +2. **Package Commands** - Replace PackageReference analysis +3. **Reference Commands** - Update ProjectReference management +4. **Workload Commands** - Replace requirement analysis +5. **Test Command** - Update project discovery + +### Phase 4: Enforcement and Cleanup +1. Update `BannedSymbols.txt` with new restrictions +2. Remove deprecated `ProjectInstanceExtensions` methods +3. Update remaining usage sites +4. Add analyzer rules for proper usage patterns + +### Phase 5: Test Infrastructure +1. Evaluate test-specific needs - may allow direct MSBuild API usage for testing implementations +2. Create test helpers using wrapper types +3. Update integration tests + +## Benefits + +### Performance Improvements +- **Evaluation Caching**: Reuse ProjectCollection across multiple project evaluations +- **Global Property Consistency**: Better cache hit rates through standardized properties +- **Resource Management**: Proper lifecycle management reduces memory pressure + +### Code Quality +- **Type Safety**: Strongly-typed access to common properties and items +- **Error Reduction**: Consistent error handling and resource management +- **Telemetry Integration**: Automatic telemetry without manual setup + +### Maintainability +- **Centralized Logic**: All MSBuild interaction through controlled interfaces +- **Best Practices**: Enforced through wrapper design and BannedApiAnalyzer +- **Documentation**: Clear usage patterns and examples + +### Developer Experience +- **Simplified API**: Higher-level abstractions for common scenarios +- **IntelliSense**: Better discovery of available properties and items +- **Consistency**: Uniform patterns across all commands + +## Implementation Notes + +### Backward Compatibility +- `ProjectInstanceExtensions` methods can be marked `[Obsolete]` initially +- Gradual migration allows testing and validation of new types +- Test infrastructure may retain direct MSBuild API access temporarily + +### Performance Considerations +- Wrapper overhead should be minimal - mostly delegation to underlying MSBuild types +- Caching benefits should outweigh abstraction costs +- Benchmark critical paths (evaluation-heavy scenarios) + +### Error Handling +- Consistent exception handling across all wrapper types +- Graceful degradation when telemetry fails +- Clear error messages for common MSBuild issues + +### Thread Safety +- `DotNetProjectEvaluator` should support concurrent project loading +- Individual `DotNetProject` instances are read-only after creation +- `DotNetProjectBuilder` instances are not thread-safe (by design) + +## Future Enhancements + +### Advanced Caching +- Persistent evaluation cache (like `VirtualProjectBuildingCommand`) +- Cross-session cache with invalidation based on file timestamps +- Distributed cache for CI scenarios + +### Performance Monitoring +- Telemetry for evaluation cache hit rates +- Performance metrics for wrapper overhead +- MSBuild API usage patterns analysis + +### Additional Wrappers +- Solution-level operations wrapper +- NuGet-specific project analysis wrapper +- Template evaluation wrapper + +## Related Work + +This plan builds upon existing work in the codebase: + +- **PR #51068**: MSBuild telemetry integration and `ProjectInstanceExtensions.cs` +- **VirtualProjectBuildingCommand**: Advanced caching patterns +- **MSBuildEvaluator**: Existing evaluation abstraction +- **BannedApiAnalyzer**: API usage enforcement infrastructure + +## Implementation Status + +### ✅ **COMPLETED** +- **Phase 1**: Core wrapper infrastructure implemented and working +- **Phase 2**: Major usage sites migrated to new wrapper types +- **Phase 4**: BannedSymbols.txt updated with new enforcement rules +- **Unit Tests**: Comprehensive test suite added + +### **Current State** +- ✅ All wrapper types (DotNetProjectEvaluator, DotNetProject, DotNetProjectBuilder, DotNetProjectItem) implemented +- ✅ Factory patterns with standard configurations (CreateForCommand, CreateForRestore, CreateForWorkloadAnalysis) +- ✅ Telemetry integration preserved and automated +- ✅ Evaluation caching enabled through ProjectCollection reuse +- ✅ 15+ command usage sites successfully migrated +- ✅ Build passes with zero errors +- ✅ BannedApiAnalyzer rules prevent future regressions + +## Success Criteria + +1. ✅ **Telemetry Integration**: 100% of MSBuild API usage includes telemetry +2. 🔄 **Performance**: Evaluation-heavy scenarios should show measurable performance improvement (needs benchmarking) +3. ✅ **Code Quality**: Reduced complexity in command implementations +4. ✅ **Compliance**: BannedApiAnalyzer prevents direct MSBuild API usage in new code +5. ✅ **Test Coverage**: Comprehensive tests for all wrapper functionality diff --git a/eng/Analyzers.props b/eng/Analyzers.props index 170e51092bb9..7c859e234fc5 100644 --- a/eng/Analyzers.props +++ b/eng/Analyzers.props @@ -1,5 +1,12 @@ + - \ No newline at end of file + + + + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index e1ef1efed51d..6f67db086b06 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -22,6 +22,7 @@ This file should be imported by eng/Versions.props 18.1.0-preview-25568-105 7.1.0-preview.1.6905 5.3.0-1.25568.105 + 5.3.0-1.25568.105 5.3.0-1.25568.105 5.3.0-1.25568.105 5.3.0-1.25568.105 @@ -214,6 +215,37 @@ This file should be imported by eng/Versions.props + + $(MicrosoftAspNetCoreMvcRazorExtensionsToolingInternalPackageVersion) + $(MicrosoftBuildPackageVersion) + $(MicrosoftBuildLocalizationPackageVersion) + $(MicrosoftBuildNuGetSdkResolverPackageVersion) + $(MicrosoftCodeAnalysisPackageVersion) + $(MicrosoftCodeAnalysisBannedApiAnalyzersPackageVersion) + $(MicrosoftCodeAnalysisBuildClientPackageVersion) + $(MicrosoftCodeAnalysisCSharpPackageVersion) + $(MicrosoftCodeAnalysisCSharpCodeStylePackageVersion) + $(MicrosoftCodeAnalysisCSharpFeaturesPackageVersion) + $(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion) + $(MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion) + $(MicrosoftCodeAnalysisPublicApiAnalyzersPackageVersion) + $(MicrosoftCodeAnalysisRazorToolingInternalPackageVersion) + $(MicrosoftCodeAnalysisWorkspacesCommonPackageVersion) + $(MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion) + $(MicrosoftDotNetArcadeSdkPackageVersion) + $(MicrosoftDotNetBuildTasksInstallersPackageVersion) + $(MicrosoftDotNetBuildTasksTemplatingPackageVersion) + $(MicrosoftDotNetBuildTasksWorkloadsPackageVersion) + $(MicrosoftDotNetHelixSdkPackageVersion) + $(MicrosoftDotNetSignToolPackageVersion) + $(MicrosoftDotNetXliffTasksPackageVersion) + $(MicrosoftDotNetXUnitExtensionsPackageVersion) + $(MicrosoftFSharpCompilerPackageVersion) + $(MicrosoftNetCompilersToolsetPackageVersion) + $(MicrosoftNetCompilersToolsetFrameworkPackageVersion) + $(MicrosoftNETRuntimeEmscriptenSdkInternalPackageVersion) + $(MicrosoftNETSdkRazorSourceGeneratorsTransportPackageVersion) + $(MicrosoftNETTestSdkPackageVersion) $(MicrosoftTemplateEngineAbstractionsPackageVersion) $(MicrosoftTemplateEngineAuthoringTemplateVerifierPackageVersion) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2ff544fb9b79..e4325b630e7f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -80,6 +80,10 @@ https://github.com/dotnet/dotnet 30f0638d1c0413878f282c0aeb4bead28497955a + + https://github.com/dotnet/dotnet + 30f0638d1c0413878f282c0aeb4bead28497955a + https://github.com/dotnet/dotnet 30f0638d1c0413878f282c0aeb4bead28497955a @@ -124,7 +128,7 @@ https://github.com/dotnet/dotnet 30f0638d1c0413878f282c0aeb4bead28497955a - + https://github.com/dotnet/dotnet 30f0638d1c0413878f282c0aeb4bead28497955a diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs index 5466d27b32c2..e27d8baeb97f 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -427,7 +427,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra } // Do not assume the change is an addition, even if the file doesn't exist in the evaluation result. - // The file could have been deleted and Add + Delete sequence could have been normalized to Update. + // The file could have been deleted and Add + Delete sequence could have been normalized to Update. return new ChangedFile( new FileItem() { FilePath = changedPath.Path, ContainingProjectPaths = [] }, changedPath.Kind); @@ -588,7 +588,7 @@ private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray items, ChangeKind kind) - => items is [{Item: var item }] + => items is [{ Item: var item }] ? GetSingularMessage(kind) + ": " + GetRelativeFilePath(item.FilePath) : GetPluralMessage(kind) + ": " + string.Join(", ", items.Select(f => GetRelativeFilePath(f.Item.FilePath))); @@ -831,7 +831,7 @@ private async ValueTask EvaluateRootProjectAsync(bool restore, var result = EvaluationResult.TryCreate( _designTimeBuildGraphFactory, - _context.RootProjectOptions.ProjectPath, + _context.RootProjectOptions.ProjectPath, _context.BuildLogger, _context.Options, _context.EnvironmentOptions, diff --git a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs index 09e33759e7a4..cad20af7c6b5 100644 --- a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs @@ -61,7 +61,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList files, using var loggers = buildReporter.GetLoggers(projectNode.ProjectInstance.FullPath, BuildTargetName); // Deep copy so that we don't pollute the project graph: - if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, loggers)) + if (!projectNode.ProjectInstance.DeepCopy().Build([BuildTargetName], loggers)) { loggers.ReportOutput(); return null; diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/MSBuildProjectExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/MSBuildProjectExtensions.cs deleted file mode 100644 index b79dcc382585..000000000000 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/MSBuildProjectExtensions.cs +++ /dev/null @@ -1,132 +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 Microsoft.Build.Construction; - -namespace Microsoft.DotNet.Cli.Utils.Extensions; - -internal static class MSBuildProjectExtensions -{ - public static bool IsConditionalOnFramework(this ProjectElement el, string framework) - { - if (!TryGetFrameworkConditionString(framework, out string? conditionStr)) - { - return el.ConditionChain().Count == 0; - } - - var condChain = el.ConditionChain(); - return condChain.Count == 1 && condChain.First().Trim() == conditionStr; - } - - public static ISet ConditionChain(this ProjectElement projectElement) - { - var conditionChainSet = new HashSet(); - - if (!string.IsNullOrEmpty(projectElement.Condition)) - { - conditionChainSet.Add(projectElement.Condition); - } - - foreach (var parent in projectElement.AllParents) - { - if (!string.IsNullOrEmpty(parent.Condition)) - { - conditionChainSet.Add(parent.Condition); - } - } - - return conditionChainSet; - } - - public static ProjectItemGroupElement? LastItemGroup(this ProjectRootElement root) - { - return root.ItemGroupsReversed.FirstOrDefault(); - } - - public static ProjectItemGroupElement FindUniformOrCreateItemGroupWithCondition(this ProjectRootElement root, string projectItemElementType, string framework) - { - var lastMatchingItemGroup = FindExistingUniformItemGroupWithCondition(root, projectItemElementType, framework); - - if (lastMatchingItemGroup != null) - { - return lastMatchingItemGroup; - } - - ProjectItemGroupElement ret = root.CreateItemGroupElement(); - if (TryGetFrameworkConditionString(framework, out string? condStr)) - { - ret.Condition = condStr; - } - - root.InsertAfterChild(ret, root.LastItemGroup()); - return ret; - } - - public static ProjectItemGroupElement? FindExistingUniformItemGroupWithCondition(this ProjectRootElement root, string projectItemElementType, string framework) - { - return root.ItemGroupsReversed.FirstOrDefault((itemGroup) => itemGroup.IsConditionalOnFramework(framework) && itemGroup.IsUniformItemElementType(projectItemElementType)); - } - - public static bool IsUniformItemElementType(this ProjectItemGroupElement group, string projectItemElementType) - { - return group.Items.All((it) => it.ItemType == projectItemElementType); - } - - public static IEnumerable FindExistingItemsWithCondition(this ProjectRootElement root, string framework, string include) - { - return root.Items.Where((el) => el.IsConditionalOnFramework(framework) && el.HasInclude(include)); - } - - public static bool HasExistingItemWithCondition(this ProjectRootElement root, string framework, string include) - { - return root.FindExistingItemsWithCondition(framework, include).Count() != 0; - } - - public static IEnumerable GetAllItemsWithElementType(this ProjectRootElement root, string projectItemElementType) - { - return root.Items.Where((it) => it.ItemType == projectItemElementType); - } - - public static bool HasInclude(this ProjectItemElement el, string include) - { - include = NormalizeIncludeForComparison(include); - foreach (var i in el.Includes()) - { - if (include == NormalizeIncludeForComparison(i)) - { - return true; - } - } - - return false; - } - - public static IEnumerable Includes( - this ProjectItemElement item) - { - return SplitSemicolonDelimitedValues(item.Include); - } - - private static IEnumerable SplitSemicolonDelimitedValues(string combinedValue) - { - return string.IsNullOrEmpty(combinedValue) ? Enumerable.Empty() : combinedValue.Split(';'); - } - - - private static bool TryGetFrameworkConditionString(string framework, out string? condition) - { - if (string.IsNullOrEmpty(framework)) - { - condition = null; - return false; - } - - condition = $"'$(TargetFramework)' == '{framework}'"; - return true; - } - - private static string NormalizeIncludeForComparison(string include) - { - return PathUtility.GetPathWithBackSlashes(include.ToLower()); - } -} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildArgs.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildArgs.cs index 7fedc17f3878..ea96f040db08 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildArgs.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildArgs.cs @@ -259,6 +259,9 @@ public MSBuildArgs CloneWithAdditionalRestoreProperties(ReadOnlyDictionary + /// Adds new global properties to the existing set of global properties for this MSBuild invocation. + /// public MSBuildArgs CloneWithAdditionalProperties(ReadOnlyDictionary? additionalProperties) { if (additionalProperties is null || additionalProperties.Count == 0) diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs index 87d021eda157..b5e1a14aec1b 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs @@ -14,10 +14,10 @@ internal sealed class MSBuildForwardingAppWithoutLogging private static readonly bool UseMSBuildServer = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_USE_MSBUILD_SERVER", false); private static readonly string? TerminalLoggerDefault = Env.GetEnvironmentVariable("DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER"); - public static string MSBuildVersion - { - get => Microsoft.Build.Evaluation.ProjectCollection.DisplayVersion; - } +#pragma warning disable RS0030 // This usage of ProjectCollection is OK because we're only using its DisplayVersion property. + public static string MSBuildVersion => Microsoft.Build.Evaluation.ProjectCollection.DisplayVersion; +#pragma warning restore RS0030 // Do not use banned APIs + private const string MSBuildExeName = "MSBuild.dll"; private const string SdksDirectoryName = "Sdks"; diff --git a/src/Cli/dotnet/CliCompletion.cs b/src/Cli/dotnet/CliCompletion.cs index 6f3f930bda97..0549ea64047f 100644 --- a/src/Cli/dotnet/CliCompletion.cs +++ b/src/Cli/dotnet/CliCompletion.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine.Completions; -using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using static System.Array; namespace Microsoft.DotNet.Cli; @@ -45,7 +43,7 @@ public static IEnumerable ProjectReferencesFromProjectFile(Compl { try { - return GetMSBuildProject()?.GetProjectToProjectReferences().Select(r => ToCompletionItem(r.Include)) ?? Empty(); + return GetMSBuildProject()?.ProjectReferences().Select(r => ToCompletionItem(r.EvaluatedInclude)) ?? Empty(); } catch (Exception) { @@ -65,12 +63,13 @@ public static IEnumerable ConfigurationsFromProjectFileOrDefault } } - private static MsbuildProject GetMSBuildProject() + private static MsbuildProject? GetMSBuildProject() { try { + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); return MsbuildProject.FromFileOrDirectory( - new ProjectCollection(), + evaluator, Directory.GetCurrentDirectory(), interactive: false); } catch (Exception e) diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/MSBuildProject.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/MSBuildProject.cs index aa64c76d1e51..e927cfe21511 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/MSBuildProject.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/MSBuildProject.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using NuGet.Frameworks; @@ -15,62 +13,20 @@ internal class MSBuildProject : IProject { private static readonly NuGetFramework s_toolPackageFramework = FrameworkConstants.CommonFrameworks.NetCoreApp10; - private readonly Project _project; + private readonly DotNetProject _project; private readonly string _msBuildExePath; - public string DepsJsonPath - { - get - { - return _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("ProjectDepsFilePath")) - .EvaluatedValue; - } - } + public string? DepsJsonPath => _project.GetPropertyValue("ProjectDepsFilePath"); - public string RuntimeConfigJsonPath - { - get - { - return _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("ProjectRuntimeConfigFilePath")) - .EvaluatedValue; - } - } + public string? RuntimeConfigJsonPath => _project.GetPropertyValue("ProjectRuntimeConfigFilePath"); - public string FullOutputPath - { - get - { - return _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("TargetDir")) - .EvaluatedValue; - } - } + public string? FullOutputPath => _project.GetPropertyValue("TargetDir"); public string ProjectRoot { get; } - public NuGetFramework DotnetCliToolTargetFramework - { - get - { - var frameworkString = _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("DotnetCliToolTargetFramework")) - ?.EvaluatedValue; - - if (string.IsNullOrEmpty(frameworkString)) - { - return s_toolPackageFramework; - } - - return NuGetFramework.Parse(frameworkString); - } - } - + public NuGetFramework DotnetCliToolTargetFramework => _project.GetPropertyValue("DotnetCliToolTargetFramework") is string s && !string.IsNullOrEmpty(s) + ? NuGetFramework.Parse(s) + : s_toolPackageFramework; public Dictionary EnvironmentVariables { @@ -83,20 +39,10 @@ public Dictionary EnvironmentVariables } } - public string ToolDepsJsonGeneratorProject - { - get - { - var generatorProject = _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("ToolDepsJsonGeneratorProject")) - ?.EvaluatedValue; - - return generatorProject; - } - } + public string? ToolDepsJsonGeneratorProject => _project.GetPropertyValue("ToolDepsJsonGeneratorProject"); - public MSBuildProject( + internal MSBuildProject( + DotNetProjectEvaluator evaluator, string msBuildProjectPath, NuGetFramework framework, string configuration, @@ -107,7 +53,7 @@ public MSBuildProject( var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "MSBuildExtensionsPath", Path.GetDirectoryName(msBuildExePath) } + { "MSBuildExtensionsPath", Path.GetDirectoryName(msBuildExePath)! } }; if (framework != null) @@ -125,19 +71,17 @@ public MSBuildProject( globalProperties.Add("Configuration", configuration); } - _project = ProjectCollection.GlobalProjectCollection.LoadProject( + _project = evaluator.LoadProject( msBuildProjectPath, - globalProperties, - null); + globalProperties); _msBuildExePath = msBuildExePath; } public IEnumerable GetTools() { - var toolsReferences = _project.AllEvaluatedItems.Where(i => i.ItemType.Equals("DotNetCliToolReference")); + var toolsReferences = _project.GetItems("DotNetCliToolReference"); var tools = toolsReferences.Select(t => new SingleProjectInfo(t.EvaluatedInclude, t.GetMetadataValue("Version"), [])); - return tools; } @@ -147,11 +91,11 @@ public LockFile GetLockFile() GetLockFilePathFromIntermediateBaseOutputPath(); return new LockFileFormat() - .ReadWithLock(lockFilePath) + .ReadWithLock(lockFilePath!) .Result; } - public bool TryGetLockFile(out LockFile lockFile) + public bool TryGetLockFile(out LockFile? lockFile) { lockFile = null; @@ -174,21 +118,15 @@ public bool TryGetLockFile(out LockFile lockFile) return true; } - private string GetLockFilePathFromProjectLockFileProperty() - { - return _project - .AllEvaluatedProperties - .Where(p => p.Name.Equals("ProjectAssetsFile")) - .Select(p => p.EvaluatedValue) - .FirstOrDefault(p => Path.IsPathRooted(p) && File.Exists(p)); - } + private string? GetLockFilePathFromProjectLockFileProperty() => _project.ProjectAssetsFile; - private string GetLockFilePathFromIntermediateBaseOutputPath() + private string? GetLockFilePathFromIntermediateBaseOutputPath() { - var intermediateOutputPath = _project - .AllEvaluatedProperties - .FirstOrDefault(p => p.Name.Equals("BaseIntermediateOutputPath")) - .EvaluatedValue; + var intermediateOutputPath = _project.GetPropertyValue("BaseIntermediateOutputPath"); + if (string.IsNullOrEmpty(intermediateOutputPath)) + { + return null; + } return Path.Combine(intermediateOutputPath, "project.assets.json"); } } diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectFactory.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectFactory.cs index 69d46a958eef..d8185529c84d 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectFactory.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectFactory.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using Microsoft.Build.Exceptions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using NuGet.Frameworks; @@ -16,7 +14,9 @@ internal class ProjectFactory(IEnvironmentProvider environment) private readonly IEnvironmentProvider _environment = environment; - public IProject GetProject( + private readonly DotNetProjectEvaluator _evaluator = new(); + + public IProject? GetProject( string projectDirectory, NuGetFramework framework, string configuration, @@ -26,7 +26,7 @@ public IProject GetProject( return GetMSBuildProj(projectDirectory, framework, configuration, outputPath); } - private IProject GetMSBuildProj(string projectDirectory, NuGetFramework framework, string configuration, string outputPath) + private IProject? GetMSBuildProj(string projectDirectory, NuGetFramework framework, string configuration, string outputPath) { var msBuildExePath = _environment.GetEnvironmentVariable(Constants.MSBUILD_EXE_PATH); @@ -39,7 +39,7 @@ private IProject GetMSBuildProj(string projectDirectory, NuGetFramework framewor ProjectFactoryName, msBuildExePath)); - string msBuildProjectPath = GetMSBuildProjPath(projectDirectory); + var msBuildProjectPath = GetMSBuildProjPath(projectDirectory); Reporter.Verbose.WriteLine(string.Format( CliStrings.MSBuildProjectPath, @@ -53,9 +53,9 @@ private IProject GetMSBuildProj(string projectDirectory, NuGetFramework framewor try { - return new MSBuildProject(msBuildProjectPath, framework, configuration, outputPath, msBuildExePath); + return new MSBuildProject(_evaluator, msBuildProjectPath, framework, configuration, outputPath, msBuildExePath); } - catch (InvalidProjectFileException ex) + catch (Exception ex) { Reporter.Verbose.WriteLine(ex.ToString().Red()); @@ -63,7 +63,7 @@ private IProject GetMSBuildProj(string projectDirectory, NuGetFramework framewor } } - private static string GetMSBuildProjPath(string projectDirectory) + private static string? GetMSBuildProjPath(string projectDirectory) { IEnumerable projectFiles = Directory .GetFiles(projectDirectory, "*.*proj") diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs index 265b1eafbb76..4cc8928fd52d 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs @@ -135,10 +135,9 @@ private void OnBuildFinished(object sender, BuildFinishedEventArgs e) internal void SendAggregatedEventsOnBuildFinished(ITelemetry? telemetry) { - if (telemetry is null) return; if (_aggregatedEvents.TryGetValue(TaskFactoryTelemetryAggregatedEventName, out var taskFactoryData)) { - Dictionary taskFactoryProperties = ConvertToStringDictionary(taskFactoryData); + var taskFactoryProperties = ConvertToStringDictionary(taskFactoryData); TrackEvent(telemetry, $"msbuild/{TaskFactoryTelemetryAggregatedEventName}", taskFactoryProperties, toBeHashed: [], toBeMeasured: []); _aggregatedEvents.Remove(TaskFactoryTelemetryAggregatedEventName); @@ -146,7 +145,7 @@ internal void SendAggregatedEventsOnBuildFinished(ITelemetry? telemetry) if (_aggregatedEvents.TryGetValue(TasksTelemetryAggregatedEventName, out var tasksData)) { - Dictionary tasksProperties = ConvertToStringDictionary(tasksData); + var tasksProperties = ConvertToStringDictionary(tasksData); TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: [], toBeMeasured: []); _aggregatedEvents.Remove(TasksTelemetryAggregatedEventName); @@ -166,10 +165,14 @@ internal void SendAggregatedEventsOnBuildFinished(ITelemetry? telemetry) internal void AggregateEvent(TelemetryEventArgs args) { - if (args.EventName is null) return; - if (!_aggregatedEvents.TryGetValue(args.EventName, out Dictionary? eventData) || eventData is null) + if (args.EventName is null) { - eventData = new Dictionary(); + return; + } + + if (!_aggregatedEvents.TryGetValue(args.EventName, out var eventData)) + { + eventData = []; _aggregatedEvents[args.EventName] = eventData; } diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluationResult.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluationResult.cs index 61bf1d418cab..0f17b866bad1 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluationResult.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluationResult.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using MSBuildProject = Microsoft.Build.Evaluation.Project; +using Microsoft.DotNet.Cli.MSBuildEvaluation; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; @@ -41,7 +41,10 @@ internal enum DotNetLanguage { NotEvaluated, CSharp, VB, FSharp } internal string? ProjectPath { get; } - public MSBuildProject? EvaluatedProject { get; protected set; } + /// + /// The evaluated MSBuild project. Null if evaluation did not succeed (so Status will be Failed/NoProjectFound/NoRestore). + /// + public DotNetProject? EvaluatedProject { get; protected set; } public string? ErrorMessage { get; protected set; } diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs index a79c2ec2af7c..3a3028c45aab 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs @@ -4,16 +4,18 @@ using System.Diagnostics; using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Utils; using MSBuildProject = Microsoft.Build.Evaluation.Project; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; internal class MSBuildEvaluator : IIdentifiedComponent { - private readonly ProjectCollection _projectCollection = new(); + private readonly DotNetProjectEvaluator _evaluator; private readonly object _lockObj = new(); private IEngineEnvironmentSettings? _settings; @@ -24,12 +26,14 @@ internal class MSBuildEvaluator : IIdentifiedComponent internal MSBuildEvaluator() { _outputDirectory = Directory.GetCurrentDirectory(); + _evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); } internal MSBuildEvaluator(string? outputDirectory = null, string? projectPath = null) { _outputDirectory = outputDirectory ?? Directory.GetCurrentDirectory(); _projectFullPath = projectPath != null ? Path.GetFullPath(projectPath) : null; + _evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); } public Guid Id => Guid.Parse("{6C2CB5CA-06C3-460A-8ADB-5F21E113AB24}"); @@ -112,26 +116,26 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin Stopwatch watch = new(); Stopwatch innerBuildWatch = new(); bool IsSdkStyleProject = false; - IReadOnlyList? targetFrameworks = null; - string? targetFramework = null; + IReadOnlyList? targetFrameworks = null; + NuGetFramework? targetFramework = null; MSBuildEvaluationResult? result = null; try { watch.Start(); _logger?.LogDebug("Evaluating project: {0}", projectPath); - MSBuildProject evaluatedProject = RunEvaluate(projectPath); + DotNetProject evaluatedProject = RunEvaluate(projectPath); //if project is using Microsoft.NET.Sdk, then it is SDK-style project. - IsSdkStyleProject = evaluatedProject.GetProperty("UsingMicrosoftNETSDK")?.EvaluatedValue == "true"; + IsSdkStyleProject = evaluatedProject.GetPropertyValue("UsingMicrosoftNETSDK") == "true"; _logger?.LogDebug("SDK-style project: {0}", IsSdkStyleProject); - targetFrameworks = evaluatedProject.GetProperty("TargetFrameworks")?.EvaluatedValue?.Split(";"); + targetFrameworks = evaluatedProject.TargetFrameworks; _logger?.LogDebug("Target frameworks: {0}", string.Join("; ", targetFrameworks ?? [])); - targetFramework = evaluatedProject.GetProperty("TargetFramework")?.EvaluatedValue; - _logger?.LogDebug("Target framework: {0}", targetFramework ?? ""); + targetFramework = evaluatedProject.TargetFramework; + _logger?.LogDebug("Target framework: {0}", targetFramework?.GetShortFolderName() ?? ""); - if (!IsSdkStyleProject || string.IsNullOrWhiteSpace(targetFramework) && targetFrameworks == null) + if (!IsSdkStyleProject || targetFramework == null && targetFrameworks == null) { //For non SDK style project, we cannot evaluate more info. Also there is no indication, whether the project //was restored or not, so it is not checked. @@ -140,14 +144,14 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin } //For SDK-style project, if the project was restored "RestoreSuccess" property will be set to true. - if (!evaluatedProject.GetProperty("RestoreSuccess")?.EvaluatedValue.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true) + if (!evaluatedProject.GetPropertyValue("RestoreSuccess")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true) { _logger?.LogDebug("Project is not restored, exiting."); return result = MSBuildEvaluationResult.CreateNoRestore(projectPath); } //If target framework is set, no further evaluation is needed. - if (!string.IsNullOrWhiteSpace(targetFramework)) + if (targetFramework != null) { _logger?.LogDebug("Project is SDK style, single TFM:{0}, evaluation succeeded.", targetFramework); return result = SDKStyleEvaluationResult.CreateSuccess(projectPath, targetFramework, evaluatedProject); @@ -162,9 +166,9 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin } //For multi-target project, we need to do additional evaluation for each target framework. - Dictionary evaluatedTfmBasedProjects = []; + Dictionary evaluatedTfmBasedProjects = []; innerBuildWatch.Start(); - foreach (string tfm in targetFrameworks) + foreach (var tfm in targetFrameworks) { _logger?.LogDebug("Evaluating project for target framework: {0}", tfm); evaluatedTfmBasedProjects[tfm] = RunEvaluate(projectPath, tfm); @@ -188,11 +192,11 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin if (targetFrameworks != null) { - targetFrameworksString = string.Join(",", targetFrameworks.Select(tfm => Sha256Hasher.HashWithNormalizedCasing(tfm))); + targetFrameworksString = string.Join(",", targetFrameworks.Select(tfm => Sha256Hasher.HashWithNormalizedCasing(tfm.GetShortFolderName()))); } else if (targetFramework != null) { - targetFrameworksString = Sha256Hasher.HashWithNormalizedCasing(targetFramework); + targetFrameworksString = Sha256Hasher.HashWithNormalizedCasing(targetFramework.GetShortFolderName()); } Dictionary properties = new() @@ -213,24 +217,13 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin } } - private MSBuildProject RunEvaluate(string projectToLoad, string? tfm = null) + private DotNetProject RunEvaluate(string projectToLoad, NuGetFramework? tfm = null) { if (!File.Exists(projectToLoad)) { throw new FileNotFoundException(message: null, projectToLoad); } - MSBuildProject? project = GetLoadedProject(projectToLoad, tfm); - if (project != null) - { - return project; - } - var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (!string.IsNullOrWhiteSpace(tfm)) - { - globalProperties["TargetFramework"] = tfm; - } - //We do only best effort here, also the evaluation should be fast vs complete; therefore ignoring imports errors. //The result of evaluation is used for the following: // - determining if the template can be run in the following context(constraints) based on Project Capabilities @@ -240,41 +233,16 @@ private MSBuildProject RunEvaluate(string projectToLoad, string? tfm = null) //- or the template content will be corrupted and consequent build fails --> the user may fix the issues manually if needed //- or the user will not see that template that is expected --> but they can always override it with --force //Therefore, we should not fail on missing imports or invalid imports, if this is the case rather restore/build should fail. - return new MSBuildProject( - projectToLoad, - globalProperties, - toolsVersion: null, - subToolsetVersion: null, - _projectCollection, - ProjectLoadSettings.IgnoreMissingImports | ProjectLoadSettings.IgnoreEmptyImports | ProjectLoadSettings.IgnoreInvalidImports); + return GetLoadedProject(projectToLoad, tfm); } - private MSBuildProject? GetLoadedProject(string projectToLoad, string? tfm) + private DotNetProject GetLoadedProject(string projectToLoad, NuGetFramework? tfm) { - MSBuildProject? project; - ICollection loadedProjects = _projectCollection.GetLoadedProjects(projectToLoad); - if (string.IsNullOrEmpty(tfm)) - { - project = loadedProjects.FirstOrDefault(project => !project.GlobalProperties.ContainsKey("TargetFramework")); - } - else + var props = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (tfm != null) { - project = loadedProjects.FirstOrDefault(project => - project.GlobalProperties.TryGetValue("TargetFramework", out string? targetFramework) - && targetFramework.Equals(tfm, StringComparison.OrdinalIgnoreCase)); - } - - if (project != null) - { - return project; - } - if (loadedProjects.Any()) - { - foreach (MSBuildProject loaded in loadedProjects) - { - _projectCollection.UnloadProject(loaded); - } + props["TargetFramework"] = tfm.GetShortFolderName(); } - return null; + return _evaluator.LoadProject(projectToLoad, props, useFlexibleLoading: true); } } diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MultiTargetEvaluationResult.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MultiTargetEvaluationResult.cs index b0e4cebafee8..9cf4f6db6715 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MultiTargetEvaluationResult.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MultiTargetEvaluationResult.cs @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using MSBuildProject = Microsoft.Build.Evaluation.Project; +using Microsoft.DotNet.Cli.MSBuildEvaluation; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; @@ -12,11 +13,11 @@ internal class MultiTargetEvaluationResult : MSBuildEvaluationResult { private MultiTargetEvaluationResult(string projectPath) : base(EvalStatus.Succeeded, projectPath) { } - internal IReadOnlyDictionary EvaluatedProjects { get; private set; } = new Dictionary(); + internal IReadOnlyDictionary EvaluatedProjects { get; private set; } = new Dictionary(); - internal IEnumerable TargetFrameworks => EvaluatedProjects.Keys; + internal IEnumerable TargetFrameworks => EvaluatedProjects.Keys; - internal static MultiTargetEvaluationResult CreateSuccess(string path, MSBuildProject project, IReadOnlyDictionary frameworkBasedResults) + internal static MultiTargetEvaluationResult CreateSuccess(string path, DotNetProject project, IReadOnlyDictionary frameworkBasedResults) { return new MultiTargetEvaluationResult(path) { diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/NonSDKStyleEvaluationResult.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/NonSDKStyleEvaluationResult.cs index be176cba29f6..278da96e46d8 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/NonSDKStyleEvaluationResult.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/NonSDKStyleEvaluationResult.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using MSBuildProject = Microsoft.Build.Evaluation.Project; +using Microsoft.DotNet.Cli.MSBuildEvaluation; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; @@ -12,11 +12,11 @@ internal class NonSDKStyleEvaluationResult : MSBuildEvaluationResult { private NonSDKStyleEvaluationResult(string projectPath) : base(EvalStatus.Succeeded, projectPath) { } - internal string? TargetFrameworkVersion => EvaluatedProject?.GetProperty("TargetFrameworkVersion").EvaluatedValue; + internal string? TargetFrameworkVersion => EvaluatedProject?.GetPropertyValue("TargetFrameworkVersion"); - internal string? PlatformTarget => EvaluatedProject?.GetProperty("PlatformTarget").EvaluatedValue; + internal string? PlatformTarget => EvaluatedProject?.GetPropertyValue("PlatformTarget"); - internal static NonSDKStyleEvaluationResult CreateSuccess(string path, MSBuildProject project) + internal static NonSDKStyleEvaluationResult CreateSuccess(string path, DotNetProject project) { return new NonSDKStyleEvaluationResult(path) { diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectCapabilityConstraint.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectCapabilityConstraint.cs index fefcc1656de3..ada5088f5497 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectCapabilityConstraint.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectCapabilityConstraint.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Constraints; using Microsoft.TemplateEngine.Cli.Commands; using Newtonsoft.Json.Linq; -using MSBuildProject = Microsoft.Build.Evaluation.Project; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; @@ -162,20 +161,20 @@ private static IReadOnlyList GetProjectCapabilities(MSBuildEvaluationRes //in case of multi-target project, consider project capabilities for all target frameworks if (result is MultiTargetEvaluationResult multiTargetResult) { - foreach (MSBuildProject? tfmBasedEvaluation in multiTargetResult.EvaluatedProjects.Values) + foreach (var tfmBasedEvaluation in multiTargetResult.EvaluatedProjects.Values) { AddProjectCapabilities(capabilities, tfmBasedEvaluation); } } return [.. capabilities]; - static void AddProjectCapabilities(HashSet collection, MSBuildProject? evaluatedProject) + static void AddProjectCapabilities(HashSet collection, DotNetProject? evaluatedProject) { if (evaluatedProject == null) { return; } - foreach (ProjectItem capability in evaluatedProject.GetItems("ProjectCapability")) + foreach (var capability in evaluatedProject.GetItems("ProjectCapability")) { if (!string.IsNullOrWhiteSpace(capability.EvaluatedInclude)) { diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectContextSymbolSource.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectContextSymbolSource.cs index 83fdcedc3d26..0610681e21b5 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectContextSymbolSource.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/ProjectContextSymbolSource.cs @@ -47,13 +47,13 @@ internal class ProjectContextSymbolSource : IBindSymbolSource return Task.FromResult((string?)null); } - string? propertyValue = evaluationResult.EvaluatedProject.GetProperty(bindname)?.EvaluatedValue; + string? propertyValue = evaluationResult.EvaluatedProject.GetPropertyValue(bindname); //we check only for null as property may exist with empty value if (propertyValue == null && evaluationResult is MultiTargetEvaluationResult multiTargetResult) { - foreach (MSBuildProject? tfmBasedProject in multiTargetResult.EvaluatedProjects.Values) + foreach (var tfmBasedProject in multiTargetResult.EvaluatedProjects.Values) { - propertyValue = evaluationResult.EvaluatedProject.GetProperty(bindname)?.EvaluatedValue; + propertyValue = tfmBasedProject.GetPropertyValue(bindname); if (propertyValue != null) { settings.Host.Logger.LogDebug("{0}: value for {1}: {2}.", nameof(ProjectContextSymbolSource), bindname, propertyValue); diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/SDKStyleEvaluationResult.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/SDKStyleEvaluationResult.cs index 14d1e524c540..2ea5ece433e7 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/SDKStyleEvaluationResult.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/SDKStyleEvaluationResult.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using MSBuildProject = Microsoft.Build.Evaluation.Project; +using Microsoft.DotNet.Cli.MSBuildEvaluation; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; @@ -12,14 +11,14 @@ namespace Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; /// internal class SDKStyleEvaluationResult : MSBuildEvaluationResult { - private SDKStyleEvaluationResult(string projectPath, string targetFramework) : base(EvalStatus.Succeeded, projectPath) + private SDKStyleEvaluationResult(string projectPath, NuGetFramework targetFramework) : base(EvalStatus.Succeeded, projectPath) { TargetFramework = targetFramework; } - internal string TargetFramework { get; } + internal NuGetFramework TargetFramework { get; } - internal static SDKStyleEvaluationResult CreateSuccess(string path, string targetFramework, MSBuildProject project) + internal static SDKStyleEvaluationResult CreateSuccess(string path, NuGetFramework targetFramework, DotNetProject project) { return new SDKStyleEvaluationResult(path, targetFramework) { diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 9f31bfa145fe..3ef2414fac78 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -94,14 +94,14 @@ public static int RunPackCommand(ParseResult parseResult) if (args.Count != 1) { - Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); + Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); return 1; } var nuspecPath = args[0]; var packArgs = new PackArgs() - { + { Logger = new NuGetConsoleLogger(), Exclude = new List(), OutputDirectory = parseResult.GetValue(PackCommandParser.OutputOption), diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index 544cd3f0fda2..53d1b9fa990d 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -4,14 +4,13 @@ using System.CommandLine; using System.Diagnostics; using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; using Microsoft.CodeAnalysis; using Microsoft.DotNet.Cli.Commands.MSBuild; using Microsoft.DotNet.Cli.Commands.NuGet; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.CommandLine; -using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.FileBasedPrograms; using NuGet.ProjectModel; @@ -192,8 +191,10 @@ private int ExecuteForFileBasedApp(string path) NoCache = true, NoBuild = true, }; - var projectCollection = new ProjectCollection(); - var projectInstance = command.CreateProjectInstance(projectCollection); + + // Include telemetry logger for project evaluation + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); + var projectInstance = command.CreateVirtualProject(evaluator); // Set initial version to Directory.Packages.props and/or C# file // (we always need to add the package reference to the C# file but when CPM is enabled, it's added without a version). @@ -216,7 +217,7 @@ private int ExecuteForFileBasedApp(string path) // If no version was specified by the user, save the actually restored version. if (specifiedVersion == null && !skipUpdate) { - var projectAssetsFile = projectInstance.GetProperty("ProjectAssetsFile")?.EvaluatedValue; + var projectAssetsFile = projectInstance.GetPropertyValue("ProjectAssetsFile"); if (!File.Exists(projectAssetsFile)) { Reporter.Verbose.WriteLine($"Assets file does not exist: {projectAssetsFile}"); @@ -273,13 +274,13 @@ void Update(string value) (Action Revert, Action Update, Action Save)? SetCentralVersion(string version) { // Find out whether CPM is enabled. - if (!MSBuildUtilities.ConvertStringToBool(projectInstance.GetProperty("ManagePackageVersionsCentrally")?.EvaluatedValue)) + if (!MSBuildUtilities.ConvertStringToBool(projectInstance.GetPropertyValue("ManagePackageVersionsCentrally"))) { return null; } // Load the Directory.Packages.props project. - var directoryPackagesPropsPath = projectInstance.GetProperty("DirectoryPackagesPropsPath")?.EvaluatedValue; + var directoryPackagesPropsPath = projectInstance.GetPropertyValue("DirectoryPackagesPropsPath"); if (!File.Exists(directoryPackagesPropsPath)) { Reporter.Verbose.WriteLine($"Directory.Packages.props file does not exist: {directoryPackagesPropsPath}"); @@ -287,23 +288,17 @@ void Update(string value) } var snapshot = File.ReadAllText(directoryPackagesPropsPath); - var directoryPackagesPropsProject = projectCollection.LoadProject(directoryPackagesPropsPath); + var directoryPackagesPropsProject = evaluator.LoadProject(directoryPackagesPropsPath); const string packageVersionItemType = "PackageVersion"; const string versionAttributeName = "Version"; // Update existing PackageVersion if it exists. - var packageVersion = directoryPackagesPropsProject.GetItems(packageVersionItemType) - .LastOrDefault(i => string.Equals(i.EvaluatedInclude, _packageId.Id, StringComparison.OrdinalIgnoreCase)); - if (packageVersion != null) + if (directoryPackagesPropsProject.TryGetPackageVersion(_packageId.Id, out var existingPackageVersion)) { - var packageVersionItemElement = packageVersion.Project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement; - var versionAttribute = packageVersionItemElement?.Metadata.FirstOrDefault(i => i.Name.Equals(versionAttributeName, StringComparison.OrdinalIgnoreCase)); - if (versionAttribute != null) + var updateResult = existingPackageVersion.UpdateSourceItem(versionAttributeName, version); + if (updateResult == DotNetProjectItem.UpdateSourceItemResult.Success) { - versionAttribute.Value = version; - directoryPackagesPropsProject.Save(); - // If user didn't specify a version and a version is already specified in Directory.Packages.props, // don't update the Directory.Packages.props (that's how the project-based equivalent behaves as well). if (specifiedVersion == null) @@ -314,30 +309,34 @@ void Update(string value) static void NoOp() { } static void Unreachable(string value) => Debug.Fail("Unreachable."); } - - return (Revert, v => Update(versionAttribute, v), Save); + else + { + return (Revert, v => Update(existingPackageVersion, v), Save); + } + } + else + { + return (Revert, v => { }, Save); } } - + else { // Get the ItemGroup to add a PackageVersion to or create a new one. - var itemGroup = directoryPackagesPropsProject.Xml.ItemGroups - .Where(e => e.Items.Any(i => string.Equals(i.ItemType, packageVersionItemType, StringComparison.OrdinalIgnoreCase))) - .FirstOrDefault() - ?? directoryPackagesPropsProject.Xml.AddItemGroup(); + if (directoryPackagesPropsProject.TryAddItem(packageVersionItemType, _packageId.Id, new(){ + [versionAttributeName] = version + }, out var item)) { - // Add a PackageVersion item. - var item = itemGroup.AddItem(packageVersionItemType, _packageId.Id); - var metadata = item.AddMetadata(versionAttributeName, version, expressAsAttribute: true); - directoryPackagesPropsProject.Save(); - - return (Revert, v => Update(metadata, v), Save); + return (Revert, v => Update(item, v), Save); + } + else + { + return (Revert, v => { }, Save); + } } - void Update(ProjectMetadataElement element, string value) + void Update(DotNetProjectItem item, string value) { - element.Value = value; - directoryPackagesPropsProject.Save(); + item.UpdateSourceItem(versionAttributeName, version); } void Revert() diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 9b7ff8d4b20a..e1dca3a46da2 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -5,9 +5,9 @@ using System.CommandLine; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.FileBasedPrograms; using Microsoft.TemplateEngine.Cli.Commands; @@ -35,19 +35,18 @@ public override int Execute() var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !_force, VirtualProjectBuildingCommand.ThrowingReporter); // Create a project instance for evaluation. - var projectCollection = new ProjectCollection(); + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); var command = new VirtualProjectBuildingCommand( entryPointFileFullPath: file, msbuildArgs: MSBuildArgs.FromOtherArgs([])) { Directives = directives, }; - var projectInstance = command.CreateProjectInstance(projectCollection); + var projectInstance = command.CreateVirtualProject(evaluator); // Evaluate directives. directives = VirtualProjectBuildingCommand.EvaluateDirectives(projectInstance, directives, sourceFile, VirtualProjectBuildingCommand.ThrowingReporter); command.Directives = directives; - projectInstance = command.CreateProjectInstance(projectCollection); // Find other items to copy over, e.g., default Content items like JSON files in Web apps. var includeItems = FindIncludedItems().ToList(); @@ -140,14 +139,14 @@ void CopyFile(string source, string target) foreach (var item in items) { // Escape hatch - exclude items that have metadata `ExcludeFromFileBasedAppConversion` set to `true`. - string include = item.GetMetadataValue("ExcludeFromFileBasedAppConversion"); + string? include = item.GetMetadataValue("ExcludeFromFileBasedAppConversion"); if (string.Equals(include, bool.TrueString, StringComparison.OrdinalIgnoreCase)) { continue; } // Exclude items that are not contained within the entry point file directory. - string itemFullPath = Path.GetFullPath(path: item.GetMetadataValue("FullPath"), basePath: entryPointFileDirectory); + string itemFullPath = Path.GetFullPath(path: item.FullPath!, basePath: entryPointFileDirectory); if (!itemFullPath.StartsWith(entryPointFileDirectory, StringComparison.OrdinalIgnoreCase)) { continue; @@ -234,7 +233,7 @@ IEnumerable FindDefaultPropertiesToExclude() { foreach (var (name, defaultValue) in VirtualProjectBuildingCommand.DefaultProperties) { - string projectValue = projectInstance.GetPropertyValue(name); + string? projectValue = projectInstance.GetPropertyValue(name); if (!string.Equals(projectValue, defaultValue, StringComparison.OrdinalIgnoreCase)) { yield return name; diff --git a/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs b/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs index 10340392c0db..7ca080bf9a1b 100644 --- a/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs +++ b/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs @@ -1,38 +1,36 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; -using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Package; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.Reference.Add; internal class ReferenceAddCommand(ParseResult parseResult) : CommandBase(parseResult) { - private readonly string _fileOrDirectory = parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? + private readonly string _fileOrDirectory = (parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? parseResult.GetValue(ReferenceCommandParser.ProjectOption) : - parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument); + parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument)) ?? Directory.GetCurrentDirectory(); public override int Execute() { - using var projects = new ProjectCollection(); + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); bool interactive = _parseResult.GetValue(ReferenceAddCommandParser.InteractiveOption); MsbuildProject msbuildProj = MsbuildProject.FromFileOrDirectory( - projects, + evaluator, _fileOrDirectory, interactive); var frameworkString = _parseResult.GetValue(ReferenceAddCommandParser.FrameworkOption); - var arguments = _parseResult.GetValue(ReferenceAddCommandParser.ProjectPathArgument).ToList().AsReadOnly(); + var arguments = _parseResult.GetRequiredValue(ReferenceAddCommandParser.ProjectPathArgument).ToList().AsReadOnly(); PathUtility.EnsureAllPathsExist(arguments, CliStrings.CouldNotFindProjectOrDirectory, true); - List refs = [.. arguments.Select((r) => MsbuildProject.FromFileOrDirectory(projects, r, interactive))]; + List refs = [.. arguments.Select((r) => MsbuildProject.FromFileOrDirectory(evaluator, r, interactive))]; if (string.IsNullOrEmpty(frameworkString)) { @@ -57,7 +55,7 @@ public override int Execute() { Reporter.Error.WriteLine(string.Format( CliStrings.ProjectDoesNotTargetFramework, - msbuildProj.ProjectRootElement.FullPath, + msbuildProj.FullPath, frameworkString)); return 1; } @@ -75,24 +73,19 @@ public override int Execute() var relativePathReferences = refs.Select((r) => Path.GetRelativePath( msbuildProj.ProjectDirectory, - r.ProjectRootElement.FullPath)).ToList(); + r.FullPath)).ToList(); int numberOfAddedReferences = msbuildProj.AddProjectToProjectReferences( frameworkString, relativePathReferences); - if (numberOfAddedReferences != 0) - { - msbuildProj.ProjectRootElement.Save(); - } - return 0; } private static string GetProjectNotCompatibleWithFrameworksDisplayString(MsbuildProject project, IEnumerable frameworksDisplayStrings) { var sb = new StringBuilder(); - sb.AppendLine(string.Format(CliStrings.ProjectNotCompatibleWithFrameworks, project.ProjectRootElement.FullPath)); + sb.AppendLine(string.Format(CliStrings.ProjectNotCompatibleWithFrameworks, project.FullPath)); foreach (var tfm in frameworksDisplayStrings) { sb.AppendLine($" - {tfm}"); diff --git a/src/Cli/dotnet/Commands/Reference/List/ReferenceListCommand.cs b/src/Cli/dotnet/Commands/Reference/List/ReferenceListCommand.cs index cbd7a84e3a73..488f71da760b 100644 --- a/src/Cli/dotnet/Commands/Reference/List/ReferenceListCommand.cs +++ b/src/Cli/dotnet/Commands/Reference/List/ReferenceListCommand.cs @@ -1,16 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Exceptions; -using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Hidden.List; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; namespace Microsoft.DotNet.Cli.Commands.Reference.List; @@ -22,26 +17,26 @@ public ReferenceListCommand(ParseResult parseResult) : base(parseResult) { ShowHelpOrErrorIfAppropriate(parseResult); - _fileOrDirectory = parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? + _fileOrDirectory = (parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? parseResult.GetValue(ReferenceCommandParser.ProjectOption) : - parseResult.GetValue(ListCommandParser.SlnOrProjectArgument) ?? - Directory.GetCurrentDirectory(); + parseResult.GetValue(ListCommandParser.SlnOrProjectArgument)) ?? + Directory.GetCurrentDirectory()!; } public override int Execute() { - var msbuildProj = MsbuildProject.FromFileOrDirectory(new ProjectCollection(), _fileOrDirectory, false); - var p2ps = msbuildProj.GetProjectToProjectReferences(); + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); + var project = evaluator.LoadProject(_fileOrDirectory); + var p2ps = project.ProjectReferences; if (!p2ps.Any()) { Reporter.Output.WriteLine(string.Format(CliStrings.NoReferencesFound, CliStrings.P2P, _fileOrDirectory)); return 0; } - ProjectInstance projectInstance = new(msbuildProj.ProjectRootElement); Reporter.Output.WriteLine($"{CliStrings.ProjectReferenceOneOrMore}"); Reporter.Output.WriteLine(new string('-', CliStrings.ProjectReferenceOneOrMore.Length)); - foreach (var item in projectInstance.GetItems("ProjectReference")) + foreach (var item in p2ps) { Reporter.Output.WriteLine(item.EvaluatedInclude); } diff --git a/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs b/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs index a6a3c6389c56..8a1177a38d57 100644 --- a/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; -using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Package; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.MSBuildEvaluation; namespace Microsoft.DotNet.Cli.Commands.Reference.Remove; @@ -19,10 +17,10 @@ internal class ReferenceRemoveCommand : CommandBase public ReferenceRemoveCommand( ParseResult parseResult) : base(parseResult) { - _fileOrDirectory = parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? + _fileOrDirectory = (parseResult.HasOption(ReferenceCommandParser.ProjectOption) ? parseResult.GetValue(ReferenceCommandParser.ProjectOption) : - parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument); - _arguments = parseResult.GetValue(ReferenceRemoveCommandParser.ProjectPathArgument).ToList().AsReadOnly(); + parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument)) ?? Directory.GetCurrentDirectory(); + _arguments = parseResult.GetRequiredValue(ReferenceRemoveCommandParser.ProjectPathArgument).ToList().AsReadOnly(); if (_arguments.Count == 0) { @@ -32,7 +30,8 @@ public ReferenceRemoveCommand( public override int Execute() { - var msbuildProj = MsbuildProject.FromFileOrDirectory(new ProjectCollection(), _fileOrDirectory, false); + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); + var msbuildProj = MsbuildProject.FromFileOrDirectory(evaluator, _fileOrDirectory, false); var references = _arguments.Select(p => { var fullPath = Path.GetFullPath(p); @@ -42,7 +41,7 @@ public override int Execute() } return Path.GetRelativePath( - msbuildProj.ProjectRootElement.FullPath, + msbuildProj.FullPath, MsbuildProject.GetProjectFileFromDirectory(fullPath) ); }); @@ -51,11 +50,6 @@ public override int Execute() _parseResult.GetValue(ReferenceRemoveCommandParser.FrameworkOption), references); - if (numberOfRemovedReferences != 0) - { - msbuildProj.ProjectRootElement.Save(); - } - return 0; } } diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index d258b9cda966..46d638963c49 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -112,7 +112,7 @@ public override RunApiOutput Execute() msbuildRestoreProperties: ReadOnlyDictionary.Empty); runCommand.TryGetLaunchProfileSettingsIfNeeded(out var launchSettings); - var targetCommand = (Utils.Command)runCommand.GetTargetCommand(buildCommand.CreateProjectInstance, cachedRunProperties: null); + var targetCommand = (Utils.Command)runCommand.GetTargetCommand(eval => buildCommand.CreateVirtualProject(eval), cachedRunProperties: null); runCommand.ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings); return new RunApiOutput.RunCommand diff --git a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs index 59ad5ce7dbba..b6157132c797 100644 --- a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs +++ b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs @@ -42,6 +42,9 @@ public static MSBuildArgs AdjustMSBuildForLLMs(MSBuildArgs msbuildArgs) /// If the environment is detected to be an LLM environment, the logger is adjusted to /// better suit that environment. /// + /// + /// Doesn't help us with 'remote logger' configuration - need https://github.com/dotnet/msbuild/pull/12827 to land for that. + /// public static Microsoft.Build.Framework.ILogger GetConsoleLogger(MSBuildArgs args) => Microsoft.Build.Logging.TerminalLogger.CreateTerminalOrConsoleLogger([.. AdjustMSBuildForLLMs(args).OtherMSBuildArgs]); } diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index e4e4fba66dc4..7d5602a71ea9 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -11,7 +11,6 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Logging; using Microsoft.DotNet.Cli.CommandFactory; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; @@ -19,8 +18,10 @@ using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.FileBasedPrograms; using Microsoft.DotNet.ProjectTools; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -129,8 +130,10 @@ public int Execute() return 1; } + using var topLevelEvaluator = DotNetProjectEvaluatorFactory.CreateForCommand(MSBuildArgs); + // Pre-run evaluation: Handle target framework selection for multi-targeted projects - if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkIfNeeded()) + if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkIfNeeded(topLevelEvaluator)) { return 1; } @@ -141,7 +144,7 @@ public int Execute() return 1; } - Func? projectFactory = null; + Func? projectFactory = null; RunProperties? cachedRunProperties = null; VirtualProjectBuildingCommand? virtualCommand = null; if (ShouldBuild) @@ -151,7 +154,7 @@ public int Execute() Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding); } - EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out virtualCommand); + (projectFactory, cachedRunProperties, virtualCommand) = EnsureProjectIsBuilt(); } else { @@ -167,7 +170,7 @@ public int Execute() virtualCommand.MarkArtifactsFolderUsed(); var cacheEntry = virtualCommand.GetPreviousCacheEntry(); - projectFactory = CanUseRunPropertiesForCscBuiltProgram(BuildLevel.None, cacheEntry) ? null : virtualCommand.CreateProjectInstance; + projectFactory = CanUseRunPropertiesForCscBuiltProgram(BuildLevel.None, cacheEntry) ? null : (eval => virtualCommand.CreateVirtualProject(eval)); cachedRunProperties = cacheEntry?.Run; } } @@ -205,16 +208,15 @@ public int Execute() /// If needed and we're in non-interactive mode, shows an error. /// /// True if we can continue, false if we should exit - private bool TrySelectTargetFrameworkIfNeeded() + private bool TrySelectTargetFrameworkIfNeeded(DotNetProjectEvaluator evaluator) { Debug.Assert(ProjectFileFullPath is not null); - var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs); if (TargetFrameworkSelector.TrySelectTargetFramework( ProjectFileFullPath, - globalProperties, + evaluator, Interactive, - out string? selectedFramework)) + out NuGetFramework? selectedFramework)) { ApplySelectedFramework(selectedFramework); return true; @@ -233,7 +235,7 @@ private bool TrySelectTargetFrameworkForFileBasedProject() Debug.Assert(EntryPointFileFullPath is not null); var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs); - + // If a framework is already specified via --framework, no need to check if (globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework)) { @@ -248,7 +250,7 @@ private bool TrySelectTargetFrameworkForFileBasedProject() } // Use TargetFrameworkSelector to handle multi-target selection (or single framework selection) - if (TargetFrameworkSelector.TrySelectTargetFramework(frameworks, Interactive, out string? selectedFramework)) + if (TargetFrameworkSelector.TrySelectTargetFramework(frameworks, Interactive, out var selectedFramework)) { ApplySelectedFramework(selectedFramework); return true; @@ -261,34 +263,34 @@ private bool TrySelectTargetFrameworkForFileBasedProject() /// Parses a source file to extract target frameworks from directives. /// /// Array of frameworks if TargetFrameworks is specified, null otherwise - private static string[]? GetTargetFrameworksFromSourceFile(string sourceFilePath) + private static NuGetFramework[]? GetTargetFrameworksFromSourceFile(string sourceFilePath) { var sourceFile = SourceFile.Load(sourceFilePath); var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, ErrorReporters.IgnoringReporter); - + var targetFrameworksDirective = directives.OfType() .FirstOrDefault(p => string.Equals(p.Name, "TargetFrameworks", StringComparison.OrdinalIgnoreCase)); - + if (targetFrameworksDirective is null) { return null; } - return targetFrameworksDirective.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return targetFrameworksDirective.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(NuGetFramework.Parse).ToArray(); } /// /// Applies the selected target framework to MSBuildArgs if a framework was provided. /// /// The framework to apply, or null if no framework selection was needed - private void ApplySelectedFramework(string? selectedFramework) + private void ApplySelectedFramework(NuGetFramework? selectedFramework) { // If selectedFramework is null, it means no framework selection was needed // (e.g., user already specified --framework, or single-target project) if (selectedFramework is not null) { var additionalProperties = new ReadOnlyDictionary( - new Dictionary { { "TargetFramework", selectedFramework } }); + new Dictionary { { "TargetFramework", selectedFramework.GetShortFolderName() } }); MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties); } } @@ -369,23 +371,25 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel return true; } - private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? virtualCommand) + private (Func? projectFactory, RunProperties? cachedRunProperties, VirtualProjectBuildingCommand? virtualCommand) EnsureProjectIsBuilt() { int buildResult; + VirtualProjectBuildingCommand? virtualCommand; + Func? projectFactory; + RunProperties? cachedRunProperties; if (EntryPointFileFullPath is not null) { virtualCommand = CreateVirtualCommand(); buildResult = virtualCommand.Execute(); - projectFactory = CanUseRunPropertiesForCscBuiltProgram(virtualCommand.LastBuild.Level, virtualCommand.LastBuild.Cache?.PreviousEntry) ? null : virtualCommand.CreateProjectInstance; + projectFactory = CanUseRunPropertiesForCscBuiltProgram(virtualCommand.LastBuild.Level, virtualCommand.LastBuild.Cache?.PreviousEntry) ? null : eval => virtualCommand.CreateVirtualProject(eval); cachedRunProperties = virtualCommand.LastBuild.Cache?.CurrentEntry.Run; } else { Debug.Assert(ProjectFileFullPath is not null); - + virtualCommand = null; projectFactory = null; cachedRunProperties = null; - virtualCommand = null; buildResult = new RestoringCommand( MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]), NoRestore, @@ -398,6 +402,7 @@ private void EnsureProjectIsBuilt(out Func? Reporter.Error.WriteLine(); throw new GracefulException(CliCommandStrings.RunCommandException); } + return (projectFactory, cachedRunProperties, virtualCommand); } private static bool CanUseRunPropertiesForCscBuiltProgram(BuildLevel level, RunFileBuildCacheEntry? previousCache) @@ -449,7 +454,7 @@ private MSBuildArgs SetupSilentBuildArgs(MSBuildArgs msbuildArgs) } } - internal ICommand GetTargetCommand(Func? projectFactory, RunProperties? cachedRunProperties) + internal ICommand GetTargetCommand(Func? projectFactory, RunProperties? cachedRunProperties) { if (cachedRunProperties != null) { @@ -470,35 +475,36 @@ internal ICommand GetTargetCommand(Func? pro Reporter.Verbose.WriteLine("Getting target command: evaluating project."); FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run"); - var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger); + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(MSBuildArgs, logger is null ? null : [logger]); + var project = EvaluateProject(ProjectFileFullPath, evaluator, projectFactory); ValidatePreconditions(project); - InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs); + InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs, evaluator); logger?.ReallyShutdown(); var runProperties = RunProperties.FromProject(project).WithApplicationArguments(ApplicationArgs); var command = CreateCommandFromRunProperties(runProperties); return command; - static ProjectInstance EvaluateProject(string? projectFilePath, Func? projectFactory, MSBuildArgs msbuildArgs, ILogger? binaryLogger) + static DotNetProject EvaluateProject(string? projectFilePath, DotNetProjectEvaluator evaluator, Func? projectFactory) { Debug.Assert(projectFilePath is not null || projectFactory is not null); - - var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs); - - var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); - + DotNetProject project; if (projectFilePath is not null) { - return collection.LoadProject(projectFilePath).CreateProjectInstance(); + project = evaluator.LoadProject(projectFilePath); + } + else + { + Debug.Assert(projectFactory is not null); + project = projectFactory(evaluator); } - Debug.Assert(projectFactory is not null); - return projectFactory(collection); + // We don't dispose the evaluator here because the telemetryCentralLogger is used later + return project; } - static void ValidatePreconditions(ProjectInstance project) + static void ValidatePreconditions(DotNetProject project) { - // there must be some kind of TFM available to run a project - if (string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework")) && string.IsNullOrEmpty(project.GetPropertyValue("TargetFrameworks"))) + if (project.TargetFramework is null || project.TargetFrameworks is null or { Length: 0 }) { ThrowUnableToRunError(project); } @@ -520,7 +526,7 @@ static ICommand CreateCommandFromRunProperties(RunProperties runProperties) return command; } - static void SetRootVariableName(ICommand command, string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, string targetFrameworkVersion) + static void SetRootVariableName(ICommand command, string? runtimeIdentifier, string? defaultAppHostRuntimeIdentifier, string? targetFrameworkVersion) { var rootVariableName = EnvironmentVariableNames.TryGetDotNetRootVariableName( runtimeIdentifier, @@ -548,7 +554,7 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s return command; } - static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs) + static void InvokeRunArgumentsTarget(DotNetProject project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, DotNetProjectEvaluator evaluator) { List loggersForBuild = [ CommonRunHelpers.GetConsoleLogger( @@ -560,7 +566,9 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca loggersForBuild.Add(binaryLogger); } - if (!project.Build([Constants.ComputeRunArguments], loggers: loggersForBuild, remoteLoggers: null, out _)) + var builder = evaluator.CreateBuilder(project); + var result = builder.Build([Constants.ComputeRunArguments], loggersForBuild); + if (!result.Success) { throw new GracefulException(CliCommandStrings.RunCommandEvaluationExceptionBuildFailed, Constants.ComputeRunArguments); } @@ -568,14 +576,14 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca } [DoesNotReturn] - internal static void ThrowUnableToRunError(ProjectInstance project) + internal static void ThrowUnableToRunError(DotNetProject project) { throw new GracefulException( string.Format( CliCommandStrings.RunCommandExceptionUnableToRun, - project.GetPropertyValue("MSBuildProjectFullPath"), + project.FullPath, Product.TargetFrameworkVersion, - project.GetPropertyValue("OutputType"))); + project.OutputType)); } private static string? DiscoverProjectFilePath(string? filePath, string? projectFileOrDirectoryPath, bool readCodeFromStdin, ref string[] args, out string? entryPointFilePath) @@ -908,8 +916,8 @@ private void SendProjectBasedTelemetry(ProjectLaunchSettingsModel? launchSetting globalProperties[Constants.EnableDefaultItems] = "false"; globalProperties[Constants.MSBuildExtensionsPath] = AppContext.BaseDirectory; - using var collection = new ProjectCollection(globalProperties: globalProperties); - var project = collection.LoadProject(ProjectFileFullPath).CreateProjectInstance(); + using var evaluator = new DotNetProjectEvaluator(globalProperties); + var project = evaluator.LoadProject(ProjectFileFullPath); packageReferenceCount = RunTelemetry.CountPackageReferences(project); projectReferenceCount = RunTelemetry.CountProjectReferences(project); diff --git a/src/Cli/dotnet/Commands/Run/RunProperties.cs b/src/Cli/dotnet/Commands/Run/RunProperties.cs index 6972d140ac56..34eb5fbaa80a 100644 --- a/src/Cli/dotnet/Commands/Run/RunProperties.cs +++ b/src/Cli/dotnet/Commands/Run/RunProperties.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Execution; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -11,19 +12,19 @@ internal sealed record RunProperties( string Command, string? Arguments, string? WorkingDirectory, - string RuntimeIdentifier, - string DefaultAppHostRuntimeIdentifier, - string TargetFrameworkVersion) + string? RuntimeIdentifier, + string? DefaultAppHostRuntimeIdentifier, + string? TargetFrameworkVersion) { internal RunProperties(string command, string? arguments, string? workingDirectory) : this(command, arguments, workingDirectory, string.Empty, string.Empty, string.Empty) { } - internal static bool TryFromProject(ProjectInstance project, [NotNullWhen(returnValue: true)] out RunProperties? result) + internal static bool TryFromProject(DotNetProject project, [NotNullWhen(returnValue: true)] out RunProperties? result) { result = new RunProperties( - Command: project.GetPropertyValue("RunCommand"), + Command: project.GetPropertyValue("RunCommand")!, Arguments: project.GetPropertyValue("RunArguments"), WorkingDirectory: project.GetPropertyValue("RunWorkingDirectory"), RuntimeIdentifier: project.GetPropertyValue("RuntimeIdentifier"), @@ -39,7 +40,7 @@ internal static bool TryFromProject(ProjectInstance project, [NotNullWhen(return return true; } - internal static RunProperties FromProject(ProjectInstance project) + internal static RunProperties FromProject(DotNetProject project) { if (!TryFromProject(project, out var result)) { diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs index 3a42cd94c06a..ed24b3493e52 100644 --- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs +++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs @@ -2,9 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.FileBasedPrograms; @@ -189,24 +188,25 @@ public static int CountProjectReferences(ImmutableArray directi return directives.OfType().Count(); } + /// - /// Counts the number of PackageReferences in a project-based app. + /// Counts the number of direct PackageReferences in a project-based app. /// - /// Project instance for project-based apps + /// DotNet project wrapper for project-based apps /// Number of package references - public static int CountPackageReferences(ProjectInstance project) + public static int CountPackageReferences(DotNetProject project) { - return project.GetItems("PackageReference").Count; + return project.GetItems("PackageReference").Count(); } /// /// Counts the number of direct ProjectReferences in a project-based app. /// - /// Project instance for project-based apps + /// DotNet project wrapper for project-based apps /// Number of project references - public static int CountProjectReferences(ProjectInstance project) + public static int CountProjectReferences(DotNetProject project) { - return project.GetItems("ProjectReference").Count; + return project.GetItems("ProjectReference").Count(); } /// diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index 82f8d7c152ba..cff906423bd1 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Build.Evaluation; -using Microsoft.Build.Exceptions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; +using NuGet.Frameworks; using Spectre.Console; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -21,43 +21,31 @@ internal static class TargetFrameworkSelector /// True if we should continue, false if we should exit with error public static bool TrySelectTargetFramework( string projectFilePath, - Dictionary globalProperties, + DotNetProjectEvaluator evaluator, bool isInteractive, - out string? selectedFramework) + out NuGetFramework? selectedFramework) { selectedFramework = null; // If a framework is already specified, no need to prompt - if (globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework)) + if (evaluator.GlobalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework)) { return true; } // Evaluate the project to get TargetFrameworks - string targetFrameworks; - try - { - using var collection = new ProjectCollection(globalProperties: globalProperties); - var project = collection.LoadProject(projectFilePath); - targetFrameworks = project.GetPropertyValue("TargetFrameworks"); - } - catch (InvalidProjectFileException) - { - // Invalid project file, return true to continue for normal error handling - return true; - } + var project = evaluator.LoadProject(projectFilePath, additionalGlobalProperties: null, useFlexibleLoading: true); + var targetFrameworks = project.TargetFrameworks; // If there's no TargetFrameworks property or only one framework, no selection needed - if (string.IsNullOrWhiteSpace(targetFrameworks)) + if (targetFrameworks is null or { Length: 0 or 1 }) { return true; } // parse the TargetFrameworks property and make sure to account for any additional whitespace // users may have added for formatting reasons. - var frameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - return TrySelectTargetFramework(frameworks, isInteractive, out selectedFramework); + return TrySelectTargetFramework(targetFrameworks, isInteractive, out selectedFramework); } /// @@ -69,7 +57,7 @@ public static bool TrySelectTargetFramework( /// Whether we're running in interactive mode (can prompt user) /// The selected target framework, or null if selection was cancelled /// True if we should continue, false if we should exit with error - public static bool TrySelectTargetFramework(string[] frameworks, bool isInteractive, out string? selectedFramework) + public static bool TrySelectTargetFramework(NuGetFramework[] frameworks, bool isInteractive, out NuGetFramework? selectedFramework) { // If there's only one framework in the TargetFrameworks, we do need to pick it to force the subsequent builds/evaluations // to act against the correct 'view' of the project @@ -107,15 +95,16 @@ public static bool TrySelectTargetFramework(string[] frameworks, bool isInteract /// /// Prompts the user to select a target framework from the available options using Spectre.Console. /// - private static string? PromptForTargetFramework(string[] frameworks) + private static NuGetFramework? PromptForTargetFramework(NuGetFramework[] frameworks) { try { - var prompt = new SelectionPrompt() + var prompt = new SelectionPrompt() .Title($"[cyan]{Markup.Escape(CliCommandStrings.RunCommandSelectTargetFrameworkPrompt)}[/]") .PageSize(10) .MoreChoicesText($"[grey]({Markup.Escape(CliCommandStrings.RunCommandMoreFrameworksText)})[/]") .AddChoices(frameworks) + .UseConverter(framework => framework.GetShortFolderName()) .EnableSearch() .SearchPlaceholderText(CliCommandStrings.RunCommandSearchPlaceholderText); diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 7721153da981..43bdd87fc1b8 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -11,7 +11,6 @@ using System.Xml; using Microsoft.Build.Construction; using Microsoft.Build.Definition; -using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Logging; @@ -20,9 +19,11 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts; using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.FileBasedPrograms; +using BuildResult = Microsoft.Build.Execution.BuildResult; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -306,21 +307,22 @@ public override int Execute() // Set up MSBuild. ReadOnlySpan binaryLoggers = binaryLogger is null ? [] : [binaryLogger.Value]; - IEnumerable loggers = [.. binaryLoggers, consoleLogger]; - var projectCollection = new ProjectCollection( - MSBuildArgs.GlobalProperties, - loggers, - ToolsetDefinitionLocations.Default); - var parameters = new BuildParameters(projectCollection) + IEnumerable existingLoggers = [.. binaryLoggers, consoleLogger]; + + // Include telemetry logger for evaluation and capture it for potential future use + var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(MSBuildArgs, existingLoggers); + var project = CreateVirtualProject(evaluator, addGlobalProperties: AddRestoreGlobalProperties(MSBuildArgs.RestoreGlobalProperties)); + var builder = evaluator.CreateBuilder(project); + var parameters = new BuildParameters(evaluator.ProjectCollection) { - Loggers = loggers, + Loggers = [evaluator.TelemetryCentralLogger, .. existingLoggers], + ForwardingLoggers = TelemetryUtilities.CreateTelemetryForwardingLoggerRecords(evaluator.TelemetryCentralLogger), LogTaskInputs = binaryLoggers.Length != 0, }; BuildManager.DefaultBuildManager.BeginBuild(parameters); int exitCode = 0; - ProjectInstance? projectInstance = null; BuildResult? buildOrRestoreResult = null; // Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`). @@ -329,10 +331,10 @@ public override int Execute() if (!NoRestore && !evalOnly) { var restoreRequest = new BuildRequestData( - CreateProjectInstance(projectCollection, addGlobalProperties: AddRestoreGlobalProperties(MSBuildArgs.RestoreGlobalProperties)), + project.Instance(), targetsToBuild: ["Restore"], hostServices: null, - BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports | BuildRequestDataFlags.FailOnUnresolvedSdk); + flags: BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports | BuildRequestDataFlags.FailOnUnresolvedSdk); var restoreResult = BuildManager.DefaultBuildManager.BuildRequest(restoreRequest); if (restoreResult.OverallResult != BuildResultCode.Success) @@ -340,7 +342,6 @@ public override int Execute() exitCode = 1; } - projectInstance = restoreRequest.ProjectInstance; buildOrRestoreResult = restoreResult; } @@ -348,7 +349,7 @@ public override int Execute() if (exitCode == 0 && !NoBuild && !evalOnly) { var buildRequest = new BuildRequestData( - CreateProjectInstance(projectCollection), + project.Instance(), targetsToBuild: RequestedTargets ?? [Constants.Build, Constants.CoreCompile]); var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest); @@ -363,11 +364,11 @@ public override int Execute() Debug.Assert(buildRequest.ProjectInstance != null); // Cache run info (to avoid re-evaluating the project instance). - cache.CurrentEntry.Run = RunProperties.TryFromProject(buildRequest.ProjectInstance, out var runProperties) + cache.CurrentEntry.Run = RunProperties.TryFromProject(project, out var runProperties) ? runProperties : null; - if (!MSBuildUtilities.ConvertStringToBool(buildRequest.ProjectInstance.GetPropertyValue(FileBasedProgramCanSkipMSBuild), defaultValue: true)) + if (!MSBuildUtilities.ConvertStringToBool(project.GetPropertyValue(FileBasedProgramCanSkipMSBuild), defaultValue: true)) { Reporter.Verbose.WriteLine($"Not saving cache because there is an opt-out via MSBuild property {FileBasedProgramCanSkipMSBuild}."); } @@ -379,15 +380,13 @@ public override int Execute() } } - projectInstance = buildRequest.ProjectInstance; buildOrRestoreResult = buildResult; } // Print build information. if (msbuildGet) { - projectInstance ??= CreateProjectInstance(projectCollection); - PrintBuildInformation(projectCollection, projectInstance, buildOrRestoreResult); + PrintBuildInformation(project, buildOrRestoreResult); } BuildManager.DefaultBuildManager.EndBuild(); @@ -515,14 +514,14 @@ static string Escape(string arg) } } - void PrintBuildInformation(ProjectCollection projectCollection, ProjectInstance projectInstance, BuildResult? buildOrRestoreResult) + void PrintBuildInformation(DotNetProject project, BuildResult? buildOrRestoreResult) { var resultOutputFile = MSBuildArgs.GetResultOutputFile is [{ } file, ..] ? file : null; // If a single property is requested, don't print as JSON. if (MSBuildArgs is { GetProperty: [{ } singlePropertyName], GetItem: null or [], GetTargetResult: null or [] }) { - var result = projectInstance.GetPropertyValue(singlePropertyName); + var result = project.GetPropertyValue(singlePropertyName); if (resultOutputFile == null) { Console.WriteLine(result); @@ -547,7 +546,7 @@ void PrintBuildInformation(ProjectCollection projectCollection, ProjectInstance foreach (var propertyName in MSBuildArgs.GetProperty) { - writer.WriteString(propertyName, projectInstance.GetPropertyValue(propertyName)); + writer.WriteString(propertyName, project.GetPropertyValue(propertyName)); } writer.WriteEndObject(); @@ -563,12 +562,12 @@ void PrintBuildInformation(ProjectCollection projectCollection, ProjectInstance writer.WritePropertyName(itemName); writer.WriteStartArray(); - foreach (var item in projectInstance.GetItems(itemName)) + foreach (var item in project.GetItems(itemName)) { writer.WriteStartObject(); writer.WriteString("Identity", item.GetMetadataValue("Identity")); - foreach (var metadatumName in item.MetadataNames) + foreach (var metadatumName in item.GetMetadataNames()) { if (metadatumName.Equals("Identity", StringComparison.OrdinalIgnoreCase)) { @@ -1054,7 +1053,7 @@ private void MarkBuildSuccess(CacheInfo cache) /// If there are any #:project , expands $() in them and ensures they point to project files (not directories). /// public static ImmutableArray EvaluateDirectives( - ProjectInstance? project, + DotNetProject? project, ImmutableArray directives, SourceFile sourceFile, ErrorReporter errorReporter) @@ -1074,48 +1073,73 @@ public static ImmutableArray EvaluateDirectives( return directives; } - public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection) - { - return CreateProjectInstance(projectCollection, addGlobalProperties: null); - } - - private ProjectInstance CreateProjectInstance( - ProjectCollection projectCollection, - Action>? addGlobalProperties) + internal DotNetProject CreateVirtualProject( + DotNetProjectEvaluator evaluator, + Action>? addGlobalProperties = null) { - var project = CreateProjectInstance(projectCollection, Directives, addGlobalProperties); + var project = CreateVirtualProject(evaluator, Directives, addGlobalProperties); var directives = EvaluateDirectives(project, Directives, EntryPointSourceFile, VirtualProjectBuildingCommand.ThrowingReporter); if (directives != Directives) { Directives = directives; - project = CreateProjectInstance(projectCollection, directives, addGlobalProperties); + project = CreateVirtualProject(evaluator, directives, addGlobalProperties); } return project; } - private ProjectInstance CreateProjectInstance( - ProjectCollection projectCollection, + private DotNetProject CreateVirtualProject( + DotNetProjectEvaluator evaluator, ImmutableArray directives, Action>? addGlobalProperties) { - var projectRoot = CreateProjectRootElement(projectCollection); - - var globalProperties = projectCollection.GlobalProperties; + var projectRoot = CreateProjectRootElement(evaluator); +#pragma warning disable RS0030 // Ok to use MSBuild APIs because we are being very limited in their scope/ + var globalProperties = evaluator.ProjectCollection.GlobalProperties; if (addGlobalProperties is not null) { - globalProperties = new Dictionary(projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); + globalProperties = new Dictionary(evaluator.ProjectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); addGlobalProperties(globalProperties); } - return ProjectInstance.FromProjectRootElement(projectRoot, new ProjectOptions + // don't load duplicate projects into the project collection + if (evaluator.ProjectCollection.LoadedProjects.FirstOrDefault(loadedProj => HasSameGlobalProperties(loadedProj, globalProperties)) is { } firstProject) { - ProjectCollection = projectCollection, + return new DotNetProject(firstProject); + } + + var p = Microsoft.Build.Evaluation.Project.FromProjectRootElement(projectRoot, new ProjectOptions + { + ProjectCollection = evaluator.ProjectCollection, GlobalProperties = globalProperties, }); +#pragma warning restore RS0030 // Do not use banned APIs + + return new DotNetProject(p); + + static bool HasSameGlobalProperties(Microsoft.Build.Evaluation.Project loadedProject, IDictionary globalProperties) + { +#pragma warning disable RS0030 // Ok to use MSBuild APIs because we are being very limited in their scope/ + if (loadedProject.Properties.Count != globalProperties.Count) + { + return false; + } + + foreach (var (key, value) in globalProperties) + { + var loadedPropertyValue = loadedProject.GetPropertyValue(key); + if (loadedPropertyValue != value) + { + return false; + } + } + + return true; +#pragma warning restore RS0030 // Do not use banned APIs + } - ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) + ProjectRootElement CreateProjectRootElement(DotNetProjectEvaluator evaluator) { var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); var projectFileWriter = new StringWriter(); @@ -1130,7 +1154,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) using var reader = new StringReader(projectFileText); using var xmlReader = XmlReader.Create(reader); - var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); + var projectRoot = ProjectRootElement.Create(xmlReader, evaluator.ProjectCollection); projectRoot.FullPath = projectFileFullPath; return projectRoot; } diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index cbd5e9588f3c..3e1058543cd2 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -7,6 +7,7 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.VisualStudio.SolutionPersistence; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -133,23 +134,25 @@ private async Task AddProjectsToSolutionAsync(IEnumerable projectPaths, } } + using var evaluator = DotNetProjectEvaluatorFactory.CreateForCommand(); + foreach (var projectPath in projectPaths) { - AddProject(solution, projectPath, serializer); + AddProject(solution, projectPath, evaluator, serializer); } await serializer.SaveAsync(_solutionFileFullPath, solution, cancellationToken); } - private void AddProject(SolutionModel solution, string fullProjectPath, ISolutionSerializer? serializer = null, bool showMessageOnDuplicate = true) + private void AddProject(SolutionModel solution, string fullProjectPath, DotNetProjectEvaluator evaluator, ISolutionSerializer? serializer = null, bool showMessageOnDuplicate = true) { string solutionRelativeProjectPath = Path.GetRelativePath(Path.GetDirectoryName(_solutionFileFullPath)!, fullProjectPath); // Open project instance to see if it is a valid project - ProjectRootElement projectRootElement; + DotNetProject evaluatedProject; try { - projectRootElement = ProjectRootElement.Open(fullProjectPath); + evaluatedProject = evaluator.LoadProject(fullProjectPath); } catch (InvalidProjectFileException ex) { @@ -157,10 +160,8 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio return; } - ProjectInstance projectInstance = new ProjectInstance(projectRootElement); - - string projectTypeGuid = solution.ProjectTypes.FirstOrDefault(t => t.Extension == Path.GetExtension(fullProjectPath))?.ProjectTypeId.ToString() - ?? projectRootElement.GetProjectTypeGuid() ?? projectInstance.GetDefaultProjectTypeGuid(); + string? projectTypeGuid = solution.ProjectTypes.FirstOrDefault(t => t.Extension == Path.GetExtension(fullProjectPath))?.ProjectTypeId.ToString() + ?? evaluatedProject.GetProjectTypeGuid() ?? evaluatedProject.GetDefaultProjectTypeGuid(); // Generate the solution folder path based on the project path SolutionFolderModel? solutionFolder = GenerateIntermediateSolutionFoldersForProjectPath(solution, solutionRelativeProjectPath); @@ -186,15 +187,15 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio } // Add settings based on existing project instance - string projectInstanceId = projectInstance.GetProjectId(); + string projectInstanceId = evaluatedProject.GetProjectId(); if (!string.IsNullOrEmpty(projectInstanceId) && serializer is ISolutionSerializer) { project.Id = new Guid(projectInstanceId); } - var projectInstanceBuildTypes = projectInstance.GetConfigurations(); - var projectInstancePlatforms = projectInstance.GetPlatforms(); + var projectInstanceBuildTypes = evaluatedProject.Configurations; + var projectInstancePlatforms = evaluatedProject.GetPlatforms(); foreach (var solutionPlatform in solution.Platforms) { @@ -213,14 +214,14 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, solutionRelativeProjectPath); // Get referencedprojects from the project instance - var referencedProjectsFullPaths = projectInstance.GetItems("ProjectReference") + var referencedProjectsFullPaths = evaluatedProject.GetItems("ProjectReference") .Select(item => Path.GetFullPath(item.EvaluatedInclude, Path.GetDirectoryName(fullProjectPath)!)); if (_includeReferences) { foreach (var referencedProjectFullPath in referencedProjectsFullPaths) { - AddProject(solution, referencedProjectFullPath, serializer, showMessageOnDuplicate: false); + AddProject(solution, referencedProjectFullPath, evaluator, serializer, showMessageOnDuplicate: false); } } } diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index fc1d2eaf2b35..f421c8c6f2f4 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -8,12 +8,14 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.DotNet.Cli.MSBuildEvaluation; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -67,12 +69,11 @@ public static (IEnumerable projects = GetProjectsProperties(collection, evaluationContext, projectPaths, buildOptions); + // Include telemetry logger for evaluation and capture it for reuse in builds + var (loggers, telemetryCentralLogger) = MSBuildEvaluation.TelemetryUtilities.CreateLoggersWithTelemetry(logger is null ? null : [logger]); + using var evaluator = MSBuildEvaluation.DotNetProjectEvaluatorFactory.Create(globalProperties, logger is null ? null : [logger]); + ConcurrentBag projects = GetProjectsProperties(evaluator, projectPaths, buildOptions); logger?.ReallyShutdown(); - collection.UnloadAllProjects(); - return (projects, isBuiltOrRestored); } @@ -89,11 +90,10 @@ public static (IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions, configuration: null, platform: null); + // Include telemetry logger for evaluation and capture it for reuse in builds + using var evaluator = MSBuildEvaluation.DotNetProjectEvaluatorFactory.Create(CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs), logger is null ? null : [logger]); + IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, evaluator, buildOptions, configuration: null, platform: null); logger?.ReallyShutdown(); - collection.UnloadAllProjects(); return (projects, isBuiltOrRestored); } @@ -161,8 +161,7 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption } private static ConcurrentBag GetProjectsProperties( - ProjectCollection projectCollection, - EvaluationContext evaluationContext, + DotNetProjectEvaluator evaluator, IEnumerable<(string ProjectFilePath, string? Configuration, string? Platform)> projects, BuildOptions buildOptions) { @@ -175,7 +174,7 @@ private static ConcurrentBag { - IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, projectCollection, evaluationContext, buildOptions, project.Configuration, project.Platform); + IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, evaluator, buildOptions, project.Configuration, project.Platform); foreach (var projectMetadata in projectsMetadata) { allProjects.Add(projectMetadata); diff --git a/src/Cli/dotnet/Commands/Test/MTP/Models.cs b/src/Cli/dotnet/Commands/Test/MTP/Models.cs index 8dae3d7cffce..eb7e4ba362ef 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Models.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Models.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -111,4 +112,4 @@ public bool MoveNext() } } -internal sealed record TestModule(RunProperties RunProperties, string? ProjectFullPath, string? TargetFramework, bool IsTestingPlatformApplication, ProjectLaunchSettingsModel? LaunchSettings, string TargetPath, string? DotnetRootArchVariableName); +internal sealed record TestModule(RunProperties RunProperties, string? ProjectFullPath, NuGetFramework? TargetFramework, bool IsTestingPlatformApplication, ProjectLaunchSettingsModel? LaunchSettings, string TargetPath, string? DotnetRootArchVariableName); diff --git a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs index 9179762e0914..984c1db0078e 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs @@ -6,8 +6,11 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; +using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.ProjectTools; @@ -153,9 +156,8 @@ private static string[] GetSolutionFilterFilePaths(string directory) private static string[] GetProjectFilePaths(string directory) => Directory.GetFiles(directory, CliConstants.ProjectExtensionPattern, SearchOption.TopDirectoryOnly); - private static ProjectInstance EvaluateProject( - ProjectCollection collection, - EvaluationContext evaluationContext, + private static DotNetProject EvaluateProject( + DotNetProjectEvaluator evaluator, string projectFilePath, string? tfm, string? configuration, @@ -200,35 +202,25 @@ private static ProjectInstance EvaluateProject( } } - // Merge the global properties from the project collection. - // It's unclear why MSBuild isn't considering the global properties defined in the ProjectCollection when - // the collection is passed in ProjectOptions below. - foreach (var property in collection.GlobalProperties) - { - if (!(globalProperties ??= new Dictionary()).ContainsKey(property.Key)) - { - globalProperties.Add(property.Key, property.Value); - } - } + return evaluator.LoadProject(projectFilePath, globalProperties); + } - return ProjectInstance.FromFile(projectFilePath, new ProjectOptions - { - GlobalProperties = globalProperties, - EvaluationContext = evaluationContext, - ProjectCollection = collection, - }); + public static string GetRootDirectory(string solutionOrProjectFilePath) + { + string? fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath); + Debug.Assert(fileDirectory is not null); + return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory; } public static IEnumerable GetProjectProperties( string projectFilePath, - ProjectCollection projectCollection, - EvaluationContext evaluationContext, + DotNetProjectEvaluator evaluator, BuildOptions buildOptions, - string? configuration, - string? platform) + string? configuration = null, + string? platform = null) { var projects = new List(); - ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, tfm: null, configuration, platform); + DotNetProject projectInstance = EvaluateProject(evaluator, projectFilePath, tfm: null, configuration, platform); var targetFramework = projectInstance.GetPropertyValue(ProjectProperties.TargetFramework); var targetFrameworks = projectInstance.GetPropertyValue(ProjectProperties.TargetFrameworks); @@ -237,7 +229,7 @@ public static IEnumerable? innerModules = null; foreach (var framework in frameworks) { - projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework, configuration, platform); + projectInstance = EvaluateProject(evaluator, projectFilePath, framework, configuration, platform); Logger.LogTrace($"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}')."); - if (GetModuleFromProject(projectInstance, buildOptions) is { } module) + if (GetModuleFromProject(projectInstance, buildOptions, evaluator) is { } module) { innerModules ??= new List(); innerModules.Add(module); @@ -296,7 +288,7 @@ public static IEnumerable { @@ -389,7 +390,7 @@ private static void AppendAssemblyResult(ITerminal terminal, TestProgressState s internal void TestCompleted( string assembly, - string? targetFramework, + NuGetFramework? targetFramework, string? architecture, string executionId, string instanceId, @@ -453,7 +454,7 @@ internal void TestCompleted( ITerminal terminal, string assembly, int attempt, - string? targetFramework, + NuGetFramework? targetFramework, string? architecture, string displayName, string? informativeMessage, @@ -632,7 +633,7 @@ private static void FormatStandardAndErrorOutput(ITerminal terminal, string? sta terminal.ResetColor(); } - private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal, string assembly, string? targetFramework, string? architecture) + private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal, string assembly, NuGetFramework? targetFramework, string? architecture) { terminal.AppendLink(assembly, lineNumber: null); if (targetFramework != null || architecture != null) @@ -640,7 +641,7 @@ private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal t terminal.Append(" ("); if (targetFramework != null) { - terminal.Append(targetFramework); + terminal.Append(targetFramework.GetShortFolderName()); if (architecture != null) { terminal.Append('|'); @@ -754,7 +755,7 @@ internal void AssemblyRunCompleted(string executionId, }); } - internal void HandshakeFailure(string assemblyPath, string? targetFramework, int exitCode, string outputData, string errorData) + internal void HandshakeFailure(string assemblyPath, NuGetFramework? targetFramework, int exitCode, string outputData, string? errorData) { if (_isHelp) { @@ -833,7 +834,7 @@ private static void AppendLongDuration(ITerminal terminal, TimeSpan duration, bo public void Dispose() => _terminalWithProgress.Dispose(); - public void ArtifactAdded(bool outOfProcess, string? assembly, string? targetFramework, string? architecture, string? executionId, string? testName, string path) + public void ArtifactAdded(bool outOfProcess, string? assembly, NuGetFramework? targetFramework, string? architecture, string? executionId, string? testName, string path) => _artifacts.Add(new TestRunArtifact(outOfProcess, assembly, targetFramework, architecture, executionId, testName, path)); /// diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressState.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressState.cs index fd801f6a5a6c..47d6b20714e7 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressState.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressState.cs @@ -2,11 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; +using NuGet.Frameworks; using TestNodeInfoEntry = (int Passed, int Skipped, int Failed, int LastAttemptNumber); namespace Microsoft.DotNet.Cli.Commands.Test.Terminal; -internal sealed class TestProgressState(long id, string assembly, string? targetFramework, string? architecture, IStopwatch stopwatch, bool isDiscovery) +internal sealed class TestProgressState(long id, string assembly, NuGetFramework? targetFramework, string? architecture, IStopwatch stopwatch, bool isDiscovery) { private readonly Dictionary _testUidToResults = new(); @@ -18,7 +19,7 @@ internal sealed class TestProgressState(long id, string assembly, string? target public string AssemblyName { get; } = Path.GetFileName(assembly)!; - public string? TargetFramework { get; } = targetFramework; + public NuGetFramework? TargetFramework { get; } = targetFramework; public string? Architecture { get; } = architecture; diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestRunArtifact.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestRunArtifact.cs index f9d2f2ef28a9..b04034114067 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestRunArtifact.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestRunArtifact.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using NuGet.Frameworks; + namespace Microsoft.DotNet.Cli.Commands.Test.Terminal; /// /// An artifact / attachment that was reported during run. /// -internal sealed record TestRunArtifact(bool OutOfProcess, string? Assembly, string? TargetFramework, string? Architecture, string? ExecutionId, string? TestName, string Path); +internal sealed record TestRunArtifact(bool OutOfProcess, string? Assembly, NuGetFramework? TargetFramework, string? Architecture, string? ExecutionId, string? TestName, string Path); diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index 9708c9246017..79671671b7ce 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -354,9 +354,9 @@ public override string ToString() builder.Append($"{ProjectProperties.ProjectFullPath}: {Module.ProjectFullPath}"); } - if (!string.IsNullOrEmpty(Module.TargetFramework)) + if (Module.TargetFramework is not null) { - builder.Append($"{ProjectProperties.TargetFramework} : {Module.TargetFramework}"); + builder.Append($"{ProjectProperties.TargetFramework} : {Module.TargetFramework.GetShortFolderName()}"); } return builder.ToString(); diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs index 8db706d7c382..0eee1169c4d0 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs @@ -3,6 +3,7 @@ using Microsoft.DotNet.Cli.Commands.Test.IPC.Models; using Microsoft.DotNet.Cli.Commands.Test.Terminal; +using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -14,7 +15,7 @@ internal sealed class TestApplicationHandler private readonly Lock _lock = new(); private readonly Dictionary _testSessionEventCountPerSessionUid = new(); - private (string? TargetFramework, string? Architecture, string ExecutionId)? _handshakeInfo; + private (NuGetFramework? TargetFramework, string? Architecture, string ExecutionId)? _handshakeInfo; public TestApplicationHandler(TerminalTestReporter output, TestModule module, TestOptions options) { @@ -31,13 +32,13 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup { _output.HandshakeFailure( _module.TargetPath, - string.Empty, + null, ExitCode.GenericFailure, string.Format( CliCommandStrings.DotnetTestIncompatibleHandshakeVersion, handshakeMessage.Properties[HandshakeMessagePropertyNames.SupportedProtocolVersions], ProtocolConstants.SupportedVersions), - string.Empty); + null); // Protocol version is not supported. // We don't attempt to do anything else. @@ -46,7 +47,7 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup var executionId = handshakeMessage.Properties[HandshakeMessagePropertyNames.ExecutionId]; var arch = handshakeMessage.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower(); - var tfm = TargetFrameworkParser.GetShortTargetFramework(handshakeMessage.Properties[HandshakeMessagePropertyNames.Framework]); + var tfm = NuGetFramework.ParseFolder(handshakeMessage.Properties[HandshakeMessagePropertyNames.Framework]); var currentHandshakeInfo = (tfm, arch, executionId); if (!_handshakeInfo.HasValue) diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 7ef075d87530..7c9e979c4b7d 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -175,7 +175,7 @@ private static int ForwardToVSTestConsole(ParseResult parseResult, string[] args public static TestCommand FromArgs(string[] args, string? testSessionCorrelationId = null, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "test", ..args]); + var parseResult = Parser.Parse(["dotnet", "test", .. args]); // settings parameters are after -- (including --), these should not be considered by the parser string[] settings = [.. args.SkipWhile(a => a != "--")]; @@ -260,7 +260,8 @@ private static TestCommand FromParseResult(ParseResult result, string[] settings Dictionary variables = VSTestForwardingApp.GetVSTestRootVariables(); - foreach (var (rootVariableName, rootValue) in variables) { + foreach (var (rootVariableName, rootValue) in variables) + { testCommand.EnvironmentVariable(rootVariableName, rootValue); VSTestTrace.SafeWriteTrace(() => $"Root variable set {rootVariableName}:{rootValue}"); } diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index 9ee9120eb642..7025d4a5adda 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -13,13 +13,13 @@ using NuGet.Frameworks; using NuGet.Versioning; using Microsoft.DotNet.Cli.Utils.Extensions; -using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.Commands.Tool.Update; using Microsoft.DotNet.Cli.Commands.Tool.Common; using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; using Microsoft.DotNet.Cli.Commands.Tool.List; +using Microsoft.DotNet.Cli.CommandLine; namespace Microsoft.DotNet.Cli.Commands.Tool.Install; @@ -188,7 +188,7 @@ private int ExecuteInstallCommand(PackageId packageId) { _reporter.WriteLine(string.Format(CliCommandStrings.ToolAlreadyInstalled, oldPackageNullable.Id, oldPackageNullable.Version.ToNormalizedString()).Green()); return 0; - } + } } TransactionalAction.Run(() => @@ -319,7 +319,7 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag { try { - uninstallAction(); + uninstallAction(); } catch (Exception ex) when (ToolUninstallCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex)) @@ -397,7 +397,7 @@ private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstal { _reporter.WriteLine( string.Format( - + newInstalledPackage.Version.IsPrerelease ? CliCommandStrings.UpdateSucceededPreVersionNoChange : CliCommandStrings.UpdateSucceededStableVersionNoChange, newInstalledPackage.Id, newInstalledPackage.Version).Green()); diff --git a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs index 1dbc16110933..dba776c00406 100644 --- a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs @@ -7,9 +7,11 @@ using Microsoft.Build.Execution; using Microsoft.Build.Logging; using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.Commands.Workload.Update; using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Sdk.WorkloadManifestReader; @@ -60,7 +62,7 @@ public override int Execute() }); workloadInstaller.Shutdown(); - + return 0; } @@ -72,24 +74,19 @@ private List RunTargetToGetWorkloadIds(IEnumerable allProjec { {"SkipResolvePackageAssets", "true"} }; - + var evaluator = DotNetProjectEvaluatorFactory.Create(globalProperties); var allWorkloadId = new List(); foreach (string projectFile in allProjects) { - var project = new ProjectInstance(projectFile, globalProperties, null); - if (!project.Targets.ContainsKey(GetRequiredWorkloadsTargetName)) + var project = evaluator.LoadProject(projectFile); + if (!project.SupportsTarget(GetRequiredWorkloadsTargetName)) { continue; } + var builder = evaluator.CreateBuilder(project); + var buildResult = builder.Build([GetRequiredWorkloadsTargetName], [ CommonRunHelpers.GetConsoleLogger(MSBuildArgs.FromVerbosity(Verbosity))]); - bool buildResult = project.Build([GetRequiredWorkloadsTargetName], - loggers: [ - new ConsoleLogger(Verbosity.ToLoggerVerbosity()) - ], - remoteLoggers: [], - targetOutputs: out var targetOutputs); - - if (buildResult == false) + if (!buildResult.Success) { throw new GracefulException( string.Format( @@ -98,7 +95,7 @@ private List RunTargetToGetWorkloadIds(IEnumerable allProjec isUserError: false); } - var targetResult = targetOutputs[GetRequiredWorkloadsTargetName]; + var targetResult = buildResult.TargetOutputs[GetRequiredWorkloadsTargetName]; allWorkloadId.AddRange(targetResult.Items.Select(item => new WorkloadId(item.ItemSpec))); } diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs index 753413943036..f64ee90e51a9 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs @@ -16,13 +16,13 @@ using Microsoft.DotNet.Cli.Commands.Workload.Search; using Microsoft.DotNet.Cli.Commands.Workload.Uninstall; using Microsoft.DotNet.Cli.Commands.Workload.Update; +using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.TemplateEngine.Cli.Commands; using IReporter = Microsoft.DotNet.Cli.Utils.IReporter; using Command = System.CommandLine.Command; -using Microsoft.DotNet.Cli.CommandLine; namespace Microsoft.DotNet.Cli.Commands.Workload; diff --git a/src/Cli/dotnet/CommonOptions.cs b/src/Cli/dotnet/CommonOptions.cs index 4547a00a730e..4816872b8e59 100644 --- a/src/Cli/dotnet/CommonOptions.cs +++ b/src/Cli/dotnet/CommonOptions.cs @@ -353,7 +353,7 @@ public static Option InteractiveOption(bool acceptArgument = false) => }; public static readonly Option> EnvOption = CreateEnvOption(CliStrings.CmdEnvironmentVariableDescription); - + public static readonly Option> TestEnvOption = CreateEnvOption(CliStrings.CmdTestEnvironmentVariableDescription); private static IReadOnlyDictionary ParseEnvironmentVariables(ArgumentResult argumentResult) diff --git a/src/Cli/dotnet/Extensions/ProjectExtensions.cs b/src/Cli/dotnet/Extensions/ProjectExtensions.cs deleted file mode 100644 index 46aedaff2468..000000000000 --- a/src/Cli/dotnet/Extensions/ProjectExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using Microsoft.Build.Evaluation; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.Cli.Extensions; - -internal static class ProjectExtensions -{ - public static IEnumerable GetRuntimeIdentifiers(this Project project) - { - return project - .GetPropertyCommaSeparatedValues("RuntimeIdentifier") - .Concat(project.GetPropertyCommaSeparatedValues("RuntimeIdentifiers")) - .Select(value => value.ToLower()) - .Distinct(); - } - - public static IEnumerable GetTargetFrameworks(this Project project) - { - var targetFrameworksStrings = project - .GetPropertyCommaSeparatedValues("TargetFramework") - .Union(project.GetPropertyCommaSeparatedValues("TargetFrameworks")) - .Select((value) => value.ToLower()); - - var uniqueTargetFrameworkStrings = new HashSet(targetFrameworksStrings); - - return uniqueTargetFrameworkStrings - .Select((frameworkString) => NuGetFramework.Parse(frameworkString)); - } - - public static IEnumerable GetConfigurations(this Project project) - { - return project.GetPropertyCommaSeparatedValues("Configurations"); - } - - public static IEnumerable GetPropertyCommaSeparatedValues(this Project project, string propertyName) - { - return project.GetPropertyValue(propertyName) - .Split(';') - .Select((value) => value.Trim()) - .Where((value) => !string.IsNullOrEmpty(value)); - } -} diff --git a/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs b/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs deleted file mode 100644 index 749007923950..000000000000 --- a/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs +++ /dev/null @@ -1,50 +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 Microsoft.Build.Execution; - -namespace Microsoft.DotNet.Cli.Extensions; - -public static class ProjectInstanceExtensions -{ - public static string GetProjectId(this ProjectInstance projectInstance) - { - var projectGuidProperty = projectInstance.GetPropertyValue("ProjectGuid"); - var projectGuid = string.IsNullOrEmpty(projectGuidProperty) - ? Guid.NewGuid() - : new Guid(projectGuidProperty); - return projectGuid.ToString("B").ToUpper(); - } - - public static string GetDefaultProjectTypeGuid(this ProjectInstance projectInstance) - { - string projectTypeGuid = projectInstance.GetPropertyValue("DefaultProjectTypeGuid"); - if (string.IsNullOrEmpty(projectTypeGuid) && projectInstance.FullPath.EndsWith(".shproj", StringComparison.OrdinalIgnoreCase)) - { - projectTypeGuid = "{D954291E-2A0B-460D-934E-DC6B0785DB48}"; - } - return projectTypeGuid; - } - - public static IEnumerable GetPlatforms(this ProjectInstance projectInstance) - { - return (projectInstance.GetPropertyValue("Platforms") ?? "") - .Split([';'], StringSplitOptions.RemoveEmptyEntries) - .Where(p => !string.IsNullOrWhiteSpace(p)) - .DefaultIfEmpty("AnyCPU"); - } - - public static IEnumerable GetConfigurations(this ProjectInstance projectInstance) - { - string foundConfig = projectInstance.GetPropertyValue("Configurations") ?? "Debug;Release"; - if (string.IsNullOrWhiteSpace(foundConfig)) - { - foundConfig = "Debug;Release"; - } - - return foundConfig - .Split([';'], StringSplitOptions.RemoveEmptyEntries) - .Where(c => !string.IsNullOrWhiteSpace(c)) - .DefaultIfEmpty("Debug"); - } -} diff --git a/src/Cli/dotnet/MSBuildEvaluation/DotNetProject.cs b/src/Cli/dotnet/MSBuildEvaluation/DotNetProject.cs new file mode 100644 index 000000000000..64f6541324bf --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/DotNetProject.cs @@ -0,0 +1,493 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable RS0030 // OK to use MSBuild APIs in this wrapper file. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Provides typed access to project properties and items from MSBuild evaluation. +/// This is a wrapper around ProjectInstance that provides a cleaner API with strongly-typed +/// access to common properties used by dotnet CLI commands. +/// +public sealed class DotNetProject(Project Project) +{ + private readonly Dictionary _itemCache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Used by raw-XML-manipulation scenarios, for example adding itemgroups/conditions. + /// + private readonly ProjectRootElement _projectRootElement = Project.Xml; + + /// + /// Gets an underlying ProjectInstance for advanced scenarios. + /// DO NOT CALL THIS generally. + /// + /// + public ProjectInstance Instance() => Project.CreateProjectInstance(); + + /// + /// Gets the full path to the project file. + /// + public string? FullPath => Project.FullPath; + + /// + /// Gets the directory containing the project file. + /// + public string? Directory => Path.GetDirectoryName(FullPath); + + // Strongly-typed access to common properties + + /// + /// Gets the target framework for the project (e.g., "net8.0"). + /// + public NuGetFramework? TargetFramework => GetPropertyValue("TargetFramework") is string tf ? NuGetFramework.Parse(tf) : null; + + /// + /// Gets all target frameworks for multi-targeting projects. + /// + public NuGetFramework[]? TargetFrameworks => GetPropertyValues("TargetFrameworks")?.Select(NuGetFramework.Parse).ToArray(); + + public string? RuntimeIdentifier => GetPropertyValue("RuntimeIdentifier"); + + public string[]? RuntimeIdentifiers => GetPropertyValues("RuntimeIdentifiers"); + + /// + /// Gets the configuration (e.g., "Debug", "Release"). + /// + public string Configuration => GetPropertyValue("Configuration") ?? "Debug"; + + /// + /// Gets the platform (e.g., "AnyCPU", "x64"). + /// + public string Platform => GetPropertyValue("Platform") ?? "AnyCPU"; + + /// + /// Gets the output type (e.g., "Exe", "Library"). + /// + public string OutputType => GetPropertyValue("OutputType") ?? ""; + + /// + /// Gets the output path for build artifacts. + /// + public string? OutputPath => GetPropertyValue("OutputPath"); + + /// + /// Gets the project assets file path (used by NuGet). + /// + public string? ProjectAssetsFile => GetPropertyValue("ProjectAssetsFile"); + + /// + /// Gets whether this project is packable. + /// + public bool IsPackable => string.Equals(GetPropertyValue("IsPackable"), "true", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets whether this project uses central package management. + /// + public bool ManagePackageVersionsCentrally => string.Equals(GetPropertyValue("ManagePackageVersionsCentrally"), "true", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the value of the specified property, or null if the property doesn't exist. + /// + /// The name of the property to retrieve. + /// The property value, or null if not found. + public string? GetPropertyValue(string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + { + return null; + } + + return Project.GetPropertyValue(propertyName); + } + + /// + /// Gets the values of a property that contains multiple semicolon-separated values. + /// + /// The name of the property to retrieve. + /// An array of property values. + public string[]? GetPropertyValues(string propertyName) + { + var value = GetPropertyValue(propertyName); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return value.Split(';', StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets all items of the specified type. + /// + /// The type of items to retrieve (e.g., "PackageReference", "ProjectReference"). + /// An enumerable of project items. + public IEnumerable GetItems(string itemType) + { + if (string.IsNullOrEmpty(itemType)) + { + return []; + } + + if (!_itemCache.TryGetValue(itemType, out var cachedItems)) + { + cachedItems = Project.GetItems(itemType) + .Select(item => new DotNetProjectItem(item)) + .ToArray(); + _itemCache[itemType] = cachedItems; + } + + return cachedItems; + } + + /// + /// Tries to find a single item of the specified type with the given include specification. If multiple are found, the 'last' one is returned. + /// + /// The type of item to find. + /// The include specification to match. + /// The found project item, or null if not found. + private bool TryFindItem(string itemType, string includeSpec, [NotNullWhen(true)] out DotNetProjectItem? item) + { + item = GetItems(itemType).LastOrDefault(i => string.Equals(i.EvaluatedInclude, includeSpec, StringComparison.OrdinalIgnoreCase)); + return item != null; + } + + /// + /// Tries to get a PackageVersion item for the specified package ID. + /// + public bool TryGetPackageVersion(string packageId, [NotNullWhen(true)] out DotNetProjectItem? item) => + TryFindItem("PackageVersion", packageId, out item); + + public IEnumerable ProjectReferences => GetItems("ProjectReference"); + + /// + /// Tries to add a new item to the project. The item will be added in the first item group that + /// contains items of the same type, or a new item group will be created if none exist. + /// + public bool TryAddItem(string itemType, string includeSpec, Dictionary? metadata, [NotNullWhen(true)] out DotNetProjectItem? item) + { + var hostItemGroup = + Project.Xml.ItemGroups + .Where(e => e.Items.Any(i => string.Equals(i.ItemType, itemType, StringComparison.OrdinalIgnoreCase))) + .FirstOrDefault() + ?? Project.Xml.AddItemGroup(); + + var rawItem = hostItemGroup.AddItem(itemType, includeSpec, metadata); + item = new DotNetProjectItem(rawItem); + return true; + } + + /// + /// Gets all available configurations for this project. + /// + public string[] Configurations => GetPropertyValue("Configurations") is string foundConfig + ? foundConfig + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .DefaultIfEmpty("Debug") + .ToArray() + : ["Debug", "Release"]; + + /// + /// Gets all available platforms for this project. + /// + public IEnumerable GetPlatforms() + { + return (GetPropertyValue("Platforms") ?? "") + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .DefaultIfEmpty("AnyCPU"); + } + + /// + /// Gets a unique identifier for this project. + /// + public string GetProjectId() + { + var projectGuidProperty = GetPropertyValue("ProjectGuid"); + var projectGuid = string.IsNullOrEmpty(projectGuidProperty) + ? Guid.NewGuid() + : new Guid(projectGuidProperty); + return projectGuid.ToString("B").ToUpper(); + } + + public string? GetProjectTypeGuid() + { + string? projectTypeGuid = GetPropertyValue("ProjectTypeGuids"); + if (!string.IsNullOrEmpty(projectTypeGuid)) + { + var firstGuid = projectTypeGuid.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (!string.IsNullOrEmpty(firstGuid)) + { + return firstGuid; + } + } + return null; + } + + /// + /// Gets the default project type GUID for this project. + /// + public string? GetDefaultProjectTypeGuid() + { + string? projectTypeGuid = GetPropertyValue("DefaultProjectTypeGuid"); + if (string.IsNullOrEmpty(projectTypeGuid) && (FullPath?.EndsWith(".shproj", StringComparison.OrdinalIgnoreCase) ?? true)) + { + projectTypeGuid = "{D954291E-2A0B-460D-934E-DC6B0785DB48}"; + } + return projectTypeGuid; + } + + /// + /// Gets whether this project is an executable project. + /// + public bool IsExecutable => string.Equals(OutputType, "Exe", StringComparison.OrdinalIgnoreCase) || + string.Equals(OutputType, "WinExe", StringComparison.OrdinalIgnoreCase); + + /// + /// Returns a string representation of this project. + /// + public override string ToString() => FullPath ?? ""; + + /// + /// Builds the project with the specified targets and loggers. Delegates to the underlying ProjectInstance directly. + /// + /// + /// NO ONE SHOULD BE CALLING THIS except the . + /// + public bool Build(ReadOnlySpan targets, IEnumerable? loggers, IEnumerable? remoteLoggers, out IDictionary targetOutputs) + { + return Instance().Build + ( + targets: targets.ToArray(), + loggers: loggers, + remoteLoggers: remoteLoggers, + targetOutputs: out targetOutputs + ); + } + + /// + /// Evaluates the provided string by expanding items and properties, as if it was found at the very end of the project file. This is useful for some hosts for which this kind of best-effort evaluation is sufficient. Does not expand bare metadata expressions. + /// + public string ExpandString(string unexpandedValue) => Project.ExpandString(unexpandedValue); + + public bool SupportsTarget(string targetName) => Project.Targets.ContainsKey(targetName); + + public enum AddType + { + Added, + AlreadyExists + } + public record struct ItemAddResult(List<(string Include, AddType AddType)> AddResult); + + /// + /// Adds items of the specified type to the project, optionally conditioned on a target framework. + /// The items are added inside the first item group that contains only items of the same type for + /// the specified framework, or a new item group is created if none exist that satisfy that condition. + /// + /// An ItemAddResult containing lists of existing and added items + public ItemAddResult AddItemsOfType(string itemType, IEnumerable<(string include, Dictionary? metadata)> itemsToAdd, string? tfmForCondition = null) + { + List<(string, AddType)> addResult = new(); + + var itemGroup = _projectRootElement.FindUniformOrCreateItemGroupWithCondition( + itemType, + tfmForCondition); + foreach (var itemData in itemsToAdd) + { + var normalizedItemRef = itemData.include.Replace('/', '\\'); + if (_projectRootElement.HasExistingItemWithCondition(tfmForCondition, normalizedItemRef)) + { + addResult.Add((itemData.include, AddType.AlreadyExists)); + continue; + } + + var itemElement = _projectRootElement.CreateItemElement(itemType, normalizedItemRef); + if (itemData.metadata != null) + { + foreach (var metadata in itemData.metadata) + { + itemElement.AddMetadata(metadata.Key, metadata.Value); + } + } + itemGroup.AppendChild(itemElement); + addResult.Add((itemData.include, AddType.Added)); + } + _projectRootElement.Save(); + Project.ReevaluateIfNecessary(); + return new(addResult); + } + + public enum RemoveType + { + Removed, + NotFound + } + + public record struct ItemRemoveResult(List<(string Include, DotNetProject.RemoveType RemoveType)> RemoveResult); + + public ItemRemoveResult RemoveItemsOfType(string itemType, IEnumerable itemsToRemove, string? tfmForCondition = null) + { + List<(string, RemoveType)> removeResult = new(); + foreach (var itemData in itemsToRemove) + { + var normalizedItemRef = itemData.Replace('/', '\\'); + var existingItems = _projectRootElement.FindExistingItemsWithCondition(tfmForCondition, normalizedItemRef); + if (existingItems.Any()) + { + foreach (var existingItem in existingItems) + { + ProjectElementContainer itemGroupParent = existingItem.Parent; + itemGroupParent.RemoveChild(existingItem); + if (itemGroupParent.Children.Count == 0) + { + itemGroupParent.Parent.RemoveChild(itemGroupParent); + } + removeResult.Add((itemData, RemoveType.Removed)); + } + continue; + } + else + { + removeResult.Add((itemData, RemoveType.NotFound)); + } + } + _projectRootElement.Save(); + Project.ReevaluateIfNecessary(); + return new(removeResult); + } +} + +/// +/// These extension methods are located here to make project file manipulation easier, but _should not_ leak out of the context of the DotNetProject. +/// +file static class MSBuildProjectExtensions +{ + public static bool IsConditionalOnFramework(this ProjectElement el, string? framework) + { + if (!TryGetFrameworkConditionString(framework, out string? conditionStr)) + { + return el.ConditionChain().Count == 0; + } + + var condChain = el.ConditionChain(); + return condChain.Count == 1 && condChain.First().Trim() == conditionStr; + } + + public static ISet ConditionChain(this ProjectElement projectElement) + { + var conditionChainSet = new HashSet(); + + if (!string.IsNullOrEmpty(projectElement.Condition)) + { + conditionChainSet.Add(projectElement.Condition); + } + + foreach (var parent in projectElement.AllParents) + { + if (!string.IsNullOrEmpty(parent.Condition)) + { + conditionChainSet.Add(parent.Condition); + } + } + + return conditionChainSet; + } + + public static ProjectItemGroupElement? LastItemGroup(this ProjectRootElement root) + { + return root.ItemGroupsReversed.FirstOrDefault(); + } + + public static ProjectItemGroupElement FindUniformOrCreateItemGroupWithCondition(this ProjectRootElement root, string projectItemElementType, string? framework) + { + var lastMatchingItemGroup = FindExistingUniformItemGroupWithCondition(root, projectItemElementType, framework); + + if (lastMatchingItemGroup != null) + { + return lastMatchingItemGroup; + } + + ProjectItemGroupElement ret = root.CreateItemGroupElement(); + if (TryGetFrameworkConditionString(framework, out string? condStr)) + { + ret.Condition = condStr; + } + + root.InsertAfterChild(ret, root.LastItemGroup()); + return ret; + } + + public static ProjectItemGroupElement? FindExistingUniformItemGroupWithCondition(this ProjectRootElement root, string projectItemElementType, string? framework) + { + return root.ItemGroupsReversed.FirstOrDefault((itemGroup) => itemGroup.IsConditionalOnFramework(framework) && itemGroup.IsUniformItemElementType(projectItemElementType)); + } + + public static bool IsUniformItemElementType(this ProjectItemGroupElement group, string projectItemElementType) + { + return group.Items.All((it) => it.ItemType == projectItemElementType); + } + + public static IEnumerable FindExistingItemsWithCondition(this ProjectRootElement root, string? framework, string include) + { + return root.Items.Where((el) => el.IsConditionalOnFramework(framework) && el.HasInclude(include)); + } + + public static bool HasExistingItemWithCondition(this ProjectRootElement root, string? framework, string include) + { + return root.FindExistingItemsWithCondition(framework, include).Count() != 0; + } + + public static IEnumerable GetAllItemsWithElementType(this ProjectRootElement root, string projectItemElementType) + { + return root.Items.Where((it) => it.ItemType == projectItemElementType); + } + + public static bool HasInclude(this ProjectItemElement el, string include) + { + include = NormalizedForComparison(include); + foreach (var i in el.Includes()) + { + if (include == NormalizedForComparison(i)) + { + return true; + } + } + + return false; + } + + public static IEnumerable Includes( + this ProjectItemElement item) + { + return SplitSemicolonDelimitedValues(item.Include); + } + + private static IEnumerable SplitSemicolonDelimitedValues(string combinedValue) + { + return string.IsNullOrEmpty(combinedValue) ? Enumerable.Empty() : combinedValue.Split(';'); + } + + + private static bool TryGetFrameworkConditionString(string? framework, out string? condition) + { + if (string.IsNullOrEmpty(framework)) + { + condition = null; + return false; + } + + condition = $"'$(TargetFramework)' == '{framework}'"; + return true; + } + + public static string NormalizedForComparison(this string include) => include.ToLower().Replace('/', '\\'); +} + diff --git a/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectBuilder.cs b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectBuilder.cs new file mode 100644 index 000000000000..8cfacb6a064f --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectBuilder.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Provides methods for building projects with automatic telemetry integration. +/// This class handles the complexity of setting up distributed logging for MSBuild. +/// +public sealed class DotNetProjectBuilder +{ + private readonly DotNetProject _project; + private readonly ILogger? _telemetryCentralLogger; + + /// + /// Initializes a new instance of the DotNetProjectBuilder class. + /// + /// The project to build. + /// The evaluator to source telemetry logger from (optional). + public DotNetProjectBuilder(DotNetProject project, DotNetProjectEvaluator? evaluator = null) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + _telemetryCentralLogger = evaluator?.TelemetryCentralLogger; + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The targets to build. + /// A BuildResult indicating success or failure. + public BuildResult Build(params string[] targets) + { + return Build(targets, additionalLoggers: null); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The targets to build. + /// Additional loggers to include. + /// A BuildResult indicating success or failure. + public BuildResult Build(string[] targets, IEnumerable? additionalLoggers) + { + var success = BuildInternal(targets, additionalLoggers, remoteLoggers: null, out var targetOutputs); + return new BuildResult(success, targetOutputs); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The targets to build. + /// The outputs from the build. + /// A BuildResult indicating success or failure. + public BuildResult Build(string[] targets, out IDictionary targetOutputs) + { + var success = BuildInternal(targets, loggers: null, remoteLoggers: null, out targetOutputs); + return new BuildResult(success, targetOutputs); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The targets to build. + /// Loggers to include. + /// The outputs from the build. + /// A BuildResult indicating success or failure. + public BuildResult Build(string[] targets, IEnumerable? loggers, out IDictionary targetOutputs) + { + var success = BuildInternal(targets, loggers, remoteLoggers: null, out targetOutputs); + return new BuildResult(success, targetOutputs); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The targets to build. + /// Loggers to include. + /// Remote/forwarding loggers to include. + /// The outputs from the build. + /// A BuildResult indicating success or failure. + public BuildResult Build( + string[] targets, + IEnumerable? loggers, + IEnumerable? remoteLoggers, + out IDictionary targetOutputs) + { + var success = BuildInternal(targets, loggers, remoteLoggers, out targetOutputs); + return new BuildResult(success, targetOutputs); + } + + private bool BuildInternal( + string[] targets, + IEnumerable? loggers, + IEnumerable? remoteLoggers, + out IDictionary targetOutputs) + { + var allLoggers = new List(); + var allForwardingLoggers = new List(); + + // Add telemetry as a distributed logger via ForwardingLoggerRecord + // Use provided central logger or create a new one + var centralLogger = _telemetryCentralLogger ?? TelemetryUtilities.CreateTelemetryCentralLogger(); + allForwardingLoggers.AddRange(TelemetryUtilities.CreateTelemetryForwardingLoggerRecords(centralLogger)); + + if (loggers != null) + { + allLoggers.AddRange(loggers); + } + + if (remoteLoggers != null) + { + allForwardingLoggers.AddRange(remoteLoggers); + } + + return _project.Build( + targets, + allLoggers.Count > 0 ? allLoggers : null, + allForwardingLoggers.Count > 0 ? allForwardingLoggers : null, + out targetOutputs); + } +} + +/// +/// Represents the result of a build operation. +/// +/// Whether the build succeeded. +/// The outputs from the build targets, if any. +public record BuildResult(bool Success, IDictionary? TargetOutputs = null); diff --git a/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluator.cs b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluator.cs new file mode 100644 index 000000000000..c7d388eac888 --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluator.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable RS0030 // Allowed to use MSBuild APIs in this file. + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Manages project evaluation with caching and consistent global properties. +/// This class provides the primary entry point for loading and evaluating projects +/// while ensuring telemetry integration and proper resource management. +/// +public sealed class DotNetProjectEvaluator : IDisposable +{ + private readonly ProjectCollection _projectCollection; + private readonly ILogger? _telemetryCentralLogger; + private readonly Dictionary _projectCache = new(StringComparer.OrdinalIgnoreCase); + private bool _disposed; + + /// + /// Initializes a new instance of the DotNetProjectEvaluator class. + /// + /// Global properties to use for all project evaluations. + /// Additional loggers to include (telemetry logger is added automatically). + public DotNetProjectEvaluator(IDictionary? globalProperties = null, IEnumerable? loggers = null) + { + var (allLoggers, telemetryCentralLogger) = TelemetryUtilities.CreateLoggersWithTelemetry(loggers); + _telemetryCentralLogger = telemetryCentralLogger; + + + _projectCollection = new ProjectCollection( + globalProperties: globalProperties, + loggers: allLoggers, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + } + + public IReadOnlyDictionary GlobalProperties => _projectCollection.GlobalProperties.AsReadOnly(); + + /// + /// Gets the telemetry central logger that can be reused for build operations. + /// + internal ILogger? TelemetryCentralLogger => _telemetryCentralLogger; + + /// + /// Gets the underlying ProjectCollection for scenarios that need direct access. + /// This should be used sparingly and only for compatibility with existing code. + /// + public ProjectCollection ProjectCollection => _projectCollection; + + /// + /// Loads and evaluates a project from the specified path. + /// Results are cached for subsequent requests with the same path and global properties. + /// + /// The path to the project file to load. + /// A DotNetProject wrapper around the loaded project. + /// Thrown when projectPath is null or empty. + /// Thrown when the project file doesn't exist. + public DotNetProject LoadProject(string projectPath) + { + return LoadProject(projectPath, additionalGlobalProperties: null); + } + + /// + /// Loads and evaluates a project from the specified path with additional global properties. + /// + /// The path to the project file to load. + /// Additional global properties to merge with the base properties. + /// If true, allows flexible loading of projects with missing imports. Defaults to false. + /// A DotNetProject wrapper around the loaded project. + /// Thrown when projectPath is null or empty. + /// Thrown when the project file doesn't exist. + public DotNetProject LoadProject(string projectPath, IDictionary? additionalGlobalProperties, bool useFlexibleLoading = false) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(DotNetProjectEvaluator)); + } + + if (string.IsNullOrEmpty(projectPath)) + { + throw new ArgumentException("Project path cannot be null or empty.", nameof(projectPath)); + } + + if (!File.Exists(projectPath)) + { + throw new FileNotFoundException($"Project file not found: {projectPath}", projectPath); + } + + // Create a cache key that includes the project path and any additional properties + string cacheKey = CreateCacheKey(projectPath, additionalGlobalProperties); + + if (!_projectCache.TryGetValue(cacheKey, out var cachedProject)) + { + // If we have additional global properties, we need to create a new ProjectCollection + // with the merged properties, otherwise we can use the existing one + ProjectCollection collectionToUse = _projectCollection; + if (additionalGlobalProperties?.Count > 0) + { + var mergedProperties = new Dictionary(_projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); + foreach (var kvp in additionalGlobalProperties) + { + mergedProperties[kvp.Key] = kvp.Value; + } + + // For now, create a temporary collection. In the future, we could cache these too. + var (allLoggers, _) = TelemetryUtilities.CreateLoggersWithTelemetry(); + collectionToUse = new ProjectCollection( + globalProperties: mergedProperties, + loggers: allLoggers, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + } + + try + { + var settings = useFlexibleLoading + ? ProjectLoadSettings.IgnoreMissingImports | ProjectLoadSettings.IgnoreEmptyImports | ProjectLoadSettings.IgnoreInvalidImports + : ProjectLoadSettings.Default; + var project = new Project(projectPath, globalProperties: null, toolsVersion: null, projectCollection: collectionToUse, loadSettings: settings); + cachedProject = new DotNetProject(project); + _projectCache[cacheKey] = cachedProject; + } + finally + { + // Dispose the temporary collection if we created one + if (collectionToUse != _projectCollection) + { + collectionToUse.Dispose(); + } + } + } + + return cachedProject; + } + + /// + /// Loads and evaluates multiple projects in parallel. + /// This is more efficient than calling LoadProject multiple times for solution scenarios. + /// + /// The paths to the project files to load. + /// An enumerable of DotNetProject wrappers. + public IEnumerable LoadProjects(IEnumerable projectPaths) + { + if (projectPaths == null) + { + throw new ArgumentNullException(nameof(projectPaths)); + } + + var paths = projectPaths.ToArray(); + if (paths.Length == 0) + { + return []; + } + + // Load projects in parallel for better performance + return paths.AsParallel().Select(LoadProject); + } + + /// + /// Creates a project builder for build operations on the specified project. + /// The builder will reuse the telemetry central logger from this evaluator. + /// + /// The project to create a builder for. + /// A DotNetProjectBuilder configured with telemetry integration. + public DotNetProjectBuilder CreateBuilder(DotNetProject project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return new DotNetProjectBuilder(project, this); + } + + private static string CreateCacheKey(string projectPath, IDictionary? additionalGlobalProperties) + { + if (additionalGlobalProperties?.Count > 0) + { + var sortedProperties = additionalGlobalProperties + .OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase) + .Select(kvp => $"{kvp.Key}={kvp.Value}"); + return $"{projectPath}|{string.Join(";", sortedProperties)}"; + } + + return projectPath; + } + + /// + /// Releases all resources used by the DotNetProjectEvaluator. + /// + public void Dispose() + { + if (!_disposed) + { + _projectCollection?.UnloadAllProjects(); + _projectCollection?.Dispose(); + _projectCache.Clear(); + _disposed = true; + } + } +} diff --git a/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluatorFactory.cs b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluatorFactory.cs new file mode 100644 index 000000000000..d28eb6bd7f10 --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectEvaluatorFactory.cs @@ -0,0 +1,159 @@ +// 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.ObjectModel; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Utils; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Factory for creating DotNetProjectEvaluator instances with standard configurations +/// used throughout the dotnet CLI commands. +/// +public static class DotNetProjectEvaluatorFactory +{ + /// + /// Creates a DotNetProjectEvaluator with standard configuration for command usage. + /// This includes MSBuildExtensionsPath and properties from command line arguments. + /// + /// Optional MSBuild arguments to extract properties from. + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator. + public static DotNetProjectEvaluator CreateForCommand(MSBuildArgs? msbuildArgs = null, IEnumerable? additionalLoggers = null) + { + var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs ?? MSBuildArgs.ForHelp); + return new DotNetProjectEvaluator(globalProperties, additionalLoggers); + } + + /// + /// Creates a DotNetProjectEvaluator optimized for restore operations. + /// This includes restore optimization properties that disable default item globbing. + /// + /// Optional MSBuild arguments to extract properties from. + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator for restore scenarios. + public static DotNetProjectEvaluator CreateForRestore(MSBuildArgs? msbuildArgs = null, IEnumerable? additionalLoggers = null) + { + var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs ?? MSBuildArgs.ForHelp); + + foreach (var kvp in RestoringCommand.RestoreOptimizationProperties) + { + globalProperties[kvp.Key] = kvp.Value; + } + + return new DotNetProjectEvaluator(globalProperties, additionalLoggers); + } + + /// + /// Creates a DotNetProjectEvaluator for template evaluation operations. + /// This configuration is used by the template engine for project analysis. + /// + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator for template scenarios. + public static DotNetProjectEvaluator CreateForTemplate(IEnumerable? additionalLoggers = null) + { + var globalProperties = GetBaseGlobalProperties(); + return new DotNetProjectEvaluator(globalProperties, additionalLoggers); + } + + /// + /// Creates a DotNetProjectEvaluator for release property detection. + /// This is used by commands like publish and pack to determine if release optimizations should be applied. + /// + /// Optional target framework to scope the evaluation to. + /// Optional configuration (Debug/Release). + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator for release property detection. + public static DotNetProjectEvaluator CreateForReleaseProperty(ReadOnlyDictionary? userProperties, string? targetFramework = null, string? configuration = null, IEnumerable? additionalLoggers = null) + { + var globalProperties = GetBaseGlobalProperties(); + if (userProperties != null) + { + globalProperties.AddRange(userProperties); + } + + if (!string.IsNullOrEmpty(targetFramework)) + { + globalProperties["TargetFramework"] = targetFramework; + } + + if (!string.IsNullOrEmpty(configuration)) + { + globalProperties["Configuration"] = configuration; + } + + return new DotNetProjectEvaluator(globalProperties, additionalLoggers); + } + + /// + /// Creates a DotNetProjectEvaluator with custom global properties. + /// + /// Custom global properties to use. + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator. + public static DotNetProjectEvaluator Create(IDictionary? globalProperties = null, IEnumerable? additionalLoggers = null) + { + var mergedProperties = GetBaseGlobalProperties(); + + if (globalProperties != null) + { + foreach (var kvp in globalProperties) + { + mergedProperties[kvp.Key] = kvp.Value; + } + } + + return new DotNetProjectEvaluator(mergedProperties, additionalLoggers); + } + + /// + /// Gets the base global properties that are common across all evaluator configurations. + /// + /// A dictionary of base global properties. + private static Dictionary GetBaseGlobalProperties() + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [Constants.MSBuildExtensionsPath] = AppContext.BaseDirectory + }; + } + + /// + /// Creates global properties from MSBuild arguments with virtual project settings. + /// This is used for scenarios where projects might not exist on disk. + /// + /// MSBuild arguments to extract properties from. + /// Additional loggers to include. + /// A configured DotNetProjectEvaluator for virtual project scenarios. + public static DotNetProjectEvaluator CreateForVirtualProject(MSBuildArgs? msbuildArgs = null, IEnumerable? additionalLoggers = null) + { + var globalProperties = GetBaseGlobalProperties(); + + // Add virtual project properties + globalProperties["_BuildNonexistentProjectsByDefault"] = "true"; + globalProperties["RestoreUseSkipNonexistentTargets"] = "false"; + globalProperties["ProvideCommandLineArgs"] = "true"; + + // Add restore optimization properties for better performance + foreach (var kvp in RestoringCommand.RestoreOptimizationProperties) + { + globalProperties[kvp.Key] = kvp.Value; + } + + // Add properties from command line arguments + if (msbuildArgs != null) + { + var argsProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs); + foreach (var kvp in argsProperties) + { + globalProperties[kvp.Key] = kvp.Value; + } + } + + return new DotNetProjectEvaluator(globalProperties, additionalLoggers); + } +} diff --git a/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectItem.cs b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectItem.cs new file mode 100644 index 000000000000..3808ed40ed04 --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/DotNetProjectItem.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable RS0030 // Ok to use MSBuild APIs in this wrapper file. + +using System.Diagnostics; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Provides typed access to project items from MSBuild evaluation, execution, construction. +/// This is a wrapper around ProjectItemInstance that provides a cleaner API. +/// +public sealed class DotNetProjectItem +{ + private readonly ProjectItem? _evalItem; + private readonly ProjectItemInstance? _executionItem; + private readonly ProjectItemElement? _constructionItem; + + public DotNetProjectItem(ProjectItem item) + { + _evalItem = item; + } + + public DotNetProjectItem(ProjectItemInstance item) + { + _executionItem = item; + } + + public DotNetProjectItem(ProjectItemElement item) + { + _constructionItem = item; + } + + /// + /// Gets the type of this item (e.g., "PackageReference", "ProjectReference", "Compile"). + /// + public string ItemType => + _evalItem is not null ? _evalItem.ItemType : + _executionItem is not null ? _executionItem.ItemType : + _constructionItem is not null ? _constructionItem.ItemType : + throw new UnreachableException(); + + /// + /// Gets the evaluated include value of this item. + /// + public string EvaluatedInclude => + _evalItem is not null ? _evalItem.EvaluatedInclude : + _executionItem is not null ? _executionItem.EvaluatedInclude : + _constructionItem is not null ? _constructionItem.Include : + throw new UnreachableException(); + + /// + /// Gets the project file that this item came from. + /// + public string? ProjectFile => + _evalItem is not null ? _evalItem.Project.FullPath : + _executionItem is not null ? _executionItem.Project.FullPath : + _constructionItem is not null ? _constructionItem.ContainingProject.FullPath : + throw new UnreachableException(); + + + /// + /// Gets the full path of this item, if available. + /// + public string? FullPath => GetMetadataValue("FullPath"); + + /// + /// Cached provenance (location) information for this item. + /// + private ProvenanceResult? _provenance => _evalItem?.Project.GetItemProvenance(_evalItem.UnevaluatedInclude, ItemType).LastOrDefault(); + + /// + /// Gets the value of the specified metadata, or null if the metadata doesn't exist. + /// + /// The name of the metadata to retrieve. + /// The metadata value, or null if not found. + public string? GetMetadataValue(string metadataName) + { + if (string.IsNullOrEmpty(metadataName)) + { + return null; + } + + return _evalItem is not null ? _evalItem.GetMetadataValue(metadataName) : _executionItem?.GetMetadataValue(metadataName); + } + + /// + /// Gets all metadata names for this item. + /// + /// An enumerable of metadata names. + public IEnumerable GetMetadataNames() { + if (_evalItem is not null) + { + if (_evalItem.MetadataCount == 0) + { + return []; + } + return _evalItem.Metadata.Select(m => m.Name); + } + else if (_executionItem is not null) + { + if (_executionItem.MetadataCount == 0) + { + return []; + } + return _executionItem.MetadataNames; + } + else if (_constructionItem is not null) + { + if (_constructionItem.Metadata.Count == 0) + { + return []; + } + return _constructionItem.Metadata.Select(m => m.Name); + } + throw new UnreachableException(); + } + + /// + /// Gets all metadata as a dictionary. + /// + /// A dictionary of metadata names and values. + public IDictionary GetMetadata() + { + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string metadataName in GetMetadataNames()) + { + metadata[metadataName] = GetMetadataValue(metadataName) ?? ""; + } + return metadata; + } + + /// + /// Returns a string representation of this item. + /// + public override string ToString() => $"{ItemType}: {EvaluatedInclude}"; + + /// + /// Updates this item's metadata value in the source project file, and saves the file after the modification + /// + /// + /// + public UpdateSourceItemResult UpdateSourceItem(string attributeName, string? value) + { + if (_evalItem is not null) + { + var sourceItem = _provenance?.ItemElement; + if (sourceItem == null) + { + return UpdateSourceItemResult.SourceItemNotFound; + } + + var versionAttribute = sourceItem?.Metadata.FirstOrDefault(i => i.Name.Equals(attributeName, StringComparison.OrdinalIgnoreCase)); + if (versionAttribute == null) + { + return UpdateSourceItemResult.MetadataNotFound; + } + + versionAttribute.Value = value; + _evalItem.Project.Save(); + + return UpdateSourceItemResult.Success; + } + else if (_executionItem is not null) + { + // ProjectItemInstance is read-only, so we cannot update it directly. + return UpdateSourceItemResult.SourceItemNotFound; + } + else if (_constructionItem is not null) + { + var versionAttribute = _constructionItem.Metadata.FirstOrDefault(i => i.Name.Equals(attributeName, StringComparison.OrdinalIgnoreCase)); + if (versionAttribute == null) + { + return UpdateSourceItemResult.MetadataNotFound; + } + + versionAttribute.Value = value; + _constructionItem.ContainingProject.Save(); + return UpdateSourceItemResult.Success; + } + throw new UnreachableException(); + } + + public enum UpdateSourceItemResult + { + Success, + MetadataNotFound, + SourceItemNotFound + } +} diff --git a/src/Cli/dotnet/MSBuildEvaluation/TelemetryUtilities.cs b/src/Cli/dotnet/MSBuildEvaluation/TelemetryUtilities.cs new file mode 100644 index 000000000000..9b09a28d4c06 --- /dev/null +++ b/src/Cli/dotnet/MSBuildEvaluation/TelemetryUtilities.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.DotNet.Cli.Commands.MSBuild; + +namespace Microsoft.DotNet.Cli.MSBuildEvaluation; + +/// +/// Internal utility class for creating and managing telemetry loggers for MSBuild operations. +/// This class consolidates the telemetry logic that was previously in ProjectInstanceExtensions. +/// +internal static class TelemetryUtilities +{ + /// + /// Creates the central telemetry logger for API-based MSBuild usage if telemetry is enabled. + /// This logger should be used for evaluation (ProjectCollection) and as a central logger for builds. + /// Returns null if telemetry is not enabled or if there's an error creating the logger. + /// + /// The central telemetry logger, or null if telemetry is disabled. + public static ILogger? CreateTelemetryCentralLogger() + { + if (Telemetry.Telemetry.CurrentSessionId != null) + { + try + { + return new MSBuildLogger(); + } + catch (Exception) + { + // Exceptions during telemetry shouldn't cause anything else to fail + } + } + return null; + } + + /// + /// Creates the forwarding logger record for distributed builds using the provided central logger. + /// This should be used with the remoteLoggers parameter of ProjectInstance.Build. + /// The same central logger instance from ProjectCollection should be reused here. + /// Returns an empty collection if the central logger is null or if there's an error. + /// + /// The central logger instance (typically the same one used in ProjectCollection). + /// An array containing the forwarding logger record, or empty array if central logger is null. + public static ForwardingLoggerRecord[] CreateTelemetryForwardingLoggerRecords(ILogger? centralLogger) + { + if (centralLogger is MSBuildLogger msbuildLogger) + { + try + { + // LoggerDescription describes the forwarding logger that worker nodes will create + var forwardingLoggerDescription = new Microsoft.Build.Logging.LoggerDescription( + loggerClassName: typeof(MSBuildForwardingLogger).FullName!, + loggerAssemblyName: typeof(MSBuildForwardingLogger).Assembly.Location, + loggerAssemblyFile: null, + loggerSwitchParameters: null, + verbosity: LoggerVerbosity.Normal); + + var loggerRecord = new ForwardingLoggerRecord(msbuildLogger, forwardingLoggerDescription); + return [loggerRecord]; + } + catch (Exception) + { + // Exceptions during telemetry shouldn't cause anything else to fail + } + } + return []; + } + + /// + /// Creates a logger collection that includes the telemetry central logger. + /// This is useful for ProjectCollection scenarios where evaluation needs telemetry. + /// Returns both the logger array and the telemetry central logger instance for reuse in subsequent builds. + /// + /// Additional loggers to include in the collection. + /// A tuple containing the logger array and the telemetry central logger (or null if no telemetry). + public static (ILogger[]? loggers, ILogger? telemetryCentralLogger) CreateLoggersWithTelemetry(IEnumerable? additionalLoggers = null) + { + var loggers = new List(); + + // Add central telemetry logger for evaluation + var telemetryCentralLogger = CreateTelemetryCentralLogger(); + if (telemetryCentralLogger != null) + { + loggers.Add(telemetryCentralLogger); + } + + if (additionalLoggers != null) + { + loggers.AddRange(additionalLoggers); + } + + return (loggers.Count > 0 ? loggers.ToArray() : null, telemetryCentralLogger); + } +} diff --git a/src/Cli/dotnet/MsbuildProject.cs b/src/Cli/dotnet/MsbuildProject.cs index bbb39ebaf2b8..14c687f95c1c 100644 --- a/src/Cli/dotnet/MsbuildProject.cs +++ b/src/Cli/dotnet/MsbuildProject.cs @@ -1,17 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Diagnostics.CodeAnalysis; -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; -using Microsoft.Build.Framework; -using Microsoft.Build.Logging; -using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.ProjectTools; using NuGet.Frameworks; @@ -21,62 +14,58 @@ internal class MsbuildProject { const string ProjectItemElementType = "ProjectReference"; - public ProjectRootElement ProjectRootElement { get; private set; } - public string ProjectDirectory { get; private set; } - - private readonly ProjectCollection _projects; - private List _cachedTfms = null; - private IEnumerable cachedRuntimeIdentifiers; - private IEnumerable cachedConfigurations; + private DotNetProject _project; + private readonly DotNetProjectEvaluator _evaluator; private readonly bool _interactive = false; - private MsbuildProject(ProjectCollection projects, ProjectRootElement project, bool interactive) + private MsbuildProject(DotNetProjectEvaluator evaluator, DotNetProject project, bool interactive) { - _projects = projects; - ProjectRootElement = project; - ProjectDirectory = PathUtility.EnsureTrailingSlash(ProjectRootElement.DirectoryPath); + _evaluator = evaluator; + _project = project; _interactive = interactive; } + public string ProjectDirectory => PathUtility.EnsureTrailingSlash(_project.Directory!); + public string FullPath => _project.FullPath!; - public static MsbuildProject FromFileOrDirectory(ProjectCollection projects, string fileOrDirectory, bool interactive) + public static MsbuildProject FromFileOrDirectory(DotNetProjectEvaluator evaluator, string fileOrDirectory, bool interactive) { if (File.Exists(fileOrDirectory)) { - return FromFile(projects, fileOrDirectory, interactive); + return FromFile(evaluator, fileOrDirectory, interactive); } else { - return FromDirectory(projects, fileOrDirectory, interactive); + return FromDirectory(evaluator, fileOrDirectory, interactive); } } - public static MsbuildProject FromFile(ProjectCollection projects, string projectPath, bool interactive) + public static MsbuildProject FromFile(DotNetProjectEvaluator evaluator, string projectPath, bool interactive) { if (!File.Exists(projectPath)) { throw new GracefulException(CliStrings.ProjectDoesNotExist, projectPath); } - var project = TryOpenProject(projects, projectPath); + var project = TryOpenProject(evaluator, projectPath, interactive); if (project == null) { throw new GracefulException(CliStrings.ProjectIsInvalid, projectPath); } - return new MsbuildProject(projects, project, interactive); + return new MsbuildProject(evaluator, project, interactive); } - public static MsbuildProject FromDirectory(ProjectCollection projects, string projectDirectory, bool interactive) + public static MsbuildProject FromDirectory(DotNetProjectEvaluator evaluator, string projectDirectory, bool interactive) { var projectFilePath = GetProjectFileFromDirectory(projectDirectory); - var project = TryOpenProject(projects, projectFilePath); + var project = TryOpenProject(evaluator, projectFilePath, interactive); if (project == null) { throw new GracefulException(CliStrings.FoundInvalidProject, projectFilePath); } - return new MsbuildProject(projects, project, interactive); + return new MsbuildProject(evaluator, project, interactive); } public static string GetProjectFileFromDirectory(string projectDirectory) @@ -84,36 +73,36 @@ public static string GetProjectFileFromDirectory(string projectDirectory) ? projectFilePath : throw new GracefulException(error); - public static bool TryGetProjectFileFromDirectory(string projectDirectory, [NotNullWhen(true)] out string projectFilePath) + public static bool TryGetProjectFileFromDirectory(string projectDirectory, [NotNullWhen(true)] out string? projectFilePath) => ProjectLocator.TryGetProjectFileFromDirectory(projectDirectory, out projectFilePath, out _); - public int AddProjectToProjectReferences(string framework, IEnumerable refs) + /// + /// Adds project-to-project references to the project. + /// + /// If set, the reference will be conditional on this framework. + /// The projects to add references to. + /// + public int AddProjectToProjectReferences(string? framework, IEnumerable refs) { - int numberOfAddedReferences = 0; - - ProjectItemGroupElement itemGroup = ProjectRootElement.FindUniformOrCreateItemGroupWithCondition( - ProjectItemElementType, - framework); - foreach (var @ref in refs.Select((r) => PathUtility.GetPathWithBackSlashes(r))) + var addResult =_project.AddItemsOfType(ProjectItemElementType, refs.Select?)>(r => (r,null)), framework); + var numberOfAddedReferences = 0; + foreach (var addItem in addResult.AddResult) { - if (ProjectRootElement.HasExistingItemWithCondition(framework, @ref)) + if (addItem.AddType == DotNetProject.AddType.Added) { - Reporter.Output.WriteLine(string.Format( - CliStrings.ProjectAlreadyHasAreference, - @ref)); - continue; + Reporter.Output.WriteLine(string.Format(CliStrings.ReferenceAddedToTheProject, addItem.Include)); + numberOfAddedReferences++; + } + else + { + Reporter.Output.WriteLine(string.Format(CliStrings.ProjectAlreadyHasAreference, addItem.Include)); } - - numberOfAddedReferences++; - itemGroup.AppendChild(ProjectRootElement.CreateItemElement(ProjectItemElementType, @ref)); - - Reporter.Output.WriteLine(string.Format(CliStrings.ReferenceAddedToTheProject, @ref)); } return numberOfAddedReferences; } - public int RemoveProjectToProjectReferences(string framework, IEnumerable refs) + public int RemoveProjectToProjectReferences(string? framework, IEnumerable refs) { int totalNumberOfRemovedReferences = 0; @@ -125,32 +114,13 @@ public int RemoveProjectToProjectReferences(string framework, IEnumerable GetProjectToProjectReferences() - { - return ProjectRootElement.GetAllItemsWithElementType(ProjectItemElementType); - } + public IEnumerable ProjectReferences() => _project.ProjectReferences; - public IEnumerable GetRuntimeIdentifiers() - { - return cachedRuntimeIdentifiers ??= GetEvaluatedProject().GetRuntimeIdentifiers(); - } + public IEnumerable GetRuntimeIdentifiers() => _project.RuntimeIdentifiers ?? []; - public IEnumerable GetTargetFrameworks() - { - if (_cachedTfms != null) - { - return _cachedTfms; - } + public IEnumerable GetTargetFrameworks() => _project.TargetFrameworks ?? []; - var project = GetEvaluatedProject(); - _cachedTfms = [.. project.GetTargetFrameworks()]; - return _cachedTfms; - } - - public IEnumerable GetConfigurations() - { - return cachedConfigurations ??= GetEvaluatedProject().GetConfigurations(); - } + public IEnumerable GetConfigurations() => _project.Configurations ?? []; public bool CanWorkOnFramework(NuGetFramework framework) { @@ -178,69 +148,27 @@ public bool IsTargetingFramework(NuGetFramework framework) return false; } - private Project GetEvaluatedProject() + private int RemoveProjectToProjectReferenceAlternatives(string? framework, string reference) { - try + var removedCount = 0; + var removeResult = _project.RemoveItemsOfType(ProjectItemElementType, GetIncludeAlternativesForRemoval(reference), framework); + foreach (var r in removeResult.RemoveResult) { - Project project; - if (_interactive) + if (r.RemoveType == DotNetProject.RemoveType.Removed) { - // NuGet need this environment variable to call plugin dll - Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", new Muxer().MuxerPath); - // Even during evaluation time, the SDK resolver may need to output auth instructions, so set a logger. - _projects.RegisterLogger(new ConsoleLogger(LoggerVerbosity.Minimal)); - project = _projects.LoadProject( - ProjectRootElement.FullPath, - new Dictionary - { ["NuGetInteractive"] = "true" }, - null); + Reporter.Output.WriteLine(string.Format(CliStrings.ProjectReferenceRemoved, r.Include)); + removedCount++; } - else - { - project = _projects.LoadProject(ProjectRootElement.FullPath); - } - - return project; } - catch (InvalidProjectFileException e) - { - throw new GracefulException(string.Format( - CliStrings.ProjectCouldNotBeEvaluated, - ProjectRootElement.FullPath, e.Message)); - } - finally - { - Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", null); - } - } - private int RemoveProjectToProjectReferenceAlternatives(string framework, string reference) - { - int numberOfRemovedRefs = 0; - foreach (var r in GetIncludeAlternativesForRemoval(reference)) - { - foreach (var existingItem in ProjectRootElement.FindExistingItemsWithCondition(framework, r)) - { - ProjectElementContainer itemGroup = existingItem.Parent; - itemGroup.RemoveChild(existingItem); - if (itemGroup.Children.Count == 0) - { - itemGroup.Parent.RemoveChild(itemGroup); - } - - numberOfRemovedRefs++; - Reporter.Output.WriteLine(string.Format(CliStrings.ProjectReferenceRemoved, r)); - } - } - - if (numberOfRemovedRefs == 0) + if (removedCount == 0) { Reporter.Output.WriteLine(string.Format( CliStrings.ProjectReferenceCouldNotBeFound, reference)); } - return numberOfRemovedRefs; + return removedCount; } // Easiest way to explain rationale for this function is on the example. Let's consider following directory structure: @@ -268,15 +196,34 @@ private IEnumerable GetIncludeAlternativesForRemoval(string reference) // There is ProjectRootElement.TryOpen but it does not work as expected // I.e. it returns null for some valid projects - private static ProjectRootElement TryOpenProject(ProjectCollection projects, string filename) + private static DotNetProject TryOpenProject(DotNetProjectEvaluator evaluator, string filename, bool interactive) { try { - return ProjectRootElement.Open(filename, projects, preserveFormatting: true); + DotNetProject project; + if (interactive) + { + // NuGet need this environment variable to call plugin dll + Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", new Muxer().MuxerPath); + // Even during evaluation time, the SDK resolver may need to output auth instructions, so set a logger. + project = evaluator.LoadProject(filename, new Dictionary(){ ["NuGetInteractive"] = "true" }); + } + else + { + project = evaluator.LoadProject(filename); + } + + return project; + } + catch (InvalidProjectFileException e) + { + throw new GracefulException(string.Format( + CliStrings.ProjectCouldNotBeEvaluated, + filename, e.Message)); } - catch (InvalidProjectFileException) + finally { - return null; + Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", null); } } } diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 2f39135d0cf2..3a33751f6c7b 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -4,9 +4,8 @@ using System.Collections.ObjectModel; using System.CommandLine; using System.Diagnostics; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.MSBuildEvaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Build.Tasks; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -40,6 +39,7 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co private static readonly string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; private static readonly string sharedProjectGuid = "{D954291E-2A0B-460D-934E-DC6B0785DB48}"; + private readonly DotNetProjectEvaluator _evaluator; // /// The boolean property to check the project for. Ex: PublishRelease, PackRelease. @@ -49,7 +49,10 @@ public ReleasePropertyProjectLocator( string propertyToCheck, DependentCommandOptions commandOptions ) - => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs); + { + (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs); + _evaluator = DotNetProjectEvaluatorFactory.CreateForReleaseProperty(parseResult.GetValue(CommonOptions.PropertiesOption), commandOptions.FrameworkOption, commandOptions.ConfigurationOption); + } /// /// Return dotnet CLI command-line parameters (or an empty list) to change configuration based on ... @@ -65,22 +68,18 @@ DependentCommandOptions commandOptions return null; } - // Analyze Global Properties - var globalProperties = GetUserSpecifiedExplicitMSBuildProperties(); - globalProperties = InjectTargetFrameworkIntoGlobalProperties(globalProperties); - // Configuration doesn't work in a .proj file, but it does as a global property. // Detect either A) --configuration option usage OR /p:Configuration=Foo, if so, don't use these properties. - if (_options.ConfigurationOption != null || globalProperties is not null && globalProperties.ContainsKey(MSBuildPropertyNames.CONFIGURATION)) + if (_options.ConfigurationOption != null || _evaluator.GlobalProperties.ContainsKey(MSBuildPropertyNames.CONFIGURATION)) return new Dictionary(1, StringComparer.OrdinalIgnoreCase) { [EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE] = "true" }.AsReadOnly(); // Don't throw error if publish* conflicts but global config specified. // Determine the project being acted upon - ProjectInstance? project = GetTargetedProject(globalProperties); + DotNetProject? project = GetTargetedProject(_evaluator); // Determine the correct value to return if (project != null) { - string propertyToCheckValue = project.GetPropertyValue(_propertyToCheck); + string? propertyToCheckValue = project.GetPropertyValue(_propertyToCheck); if (!string.IsNullOrEmpty(propertyToCheckValue)) { var newConfigurationArgs = new Dictionary(2, StringComparer.OrdinalIgnoreCase); @@ -106,29 +105,29 @@ DependentCommandOptions commandOptions /// /// A project instance that will be targeted to publish/pack, etc. null if one does not exist. /// Will return an arbitrary project in the solution if one exists in the solution and there's no project targeted. - public ProjectInstance? GetTargetedProject(ReadOnlyDictionary? globalProps) + public DotNetProject? GetTargetedProject(DotNetProjectEvaluator evaluator) { foreach (string arg in _slnOrProjectArgs.Append(Directory.GetCurrentDirectory())) { if (VirtualProjectBuildingCommand.IsValidEntryPointPath(arg)) { - return new VirtualProjectBuildingCommand(Path.GetFullPath(arg), MSBuildArgs.FromProperties(globalProps)) - .CreateProjectInstance(ProjectCollection.GlobalProjectCollection); + return new VirtualProjectBuildingCommand(Path.GetFullPath(arg), MSBuildArgs.FromProperties(new Dictionary(evaluator.GlobalProperties).AsReadOnly())) + .CreateVirtualProject(evaluator); } else if (IsValidProjectFilePath(arg)) { - return TryGetProjectInstance(arg, globalProps); + return TryGetProjectInstance(arg, evaluator); } else if (IsValidSlnFilePath(arg)) { - return GetArbitraryProjectFromSolution(arg, globalProps); + return GetArbitraryProjectFromSolution(arg, evaluator); } else if (Directory.Exists(arg)) // Get here if the user did not provide a .proj or a .sln. (See CWD appended to args above) { // First, look for a project in the directory. if (MsbuildProject.TryGetProjectFileFromDirectory(arg, out var projectFilePath)) { - return TryGetProjectInstance(projectFilePath, globalProps); + return TryGetProjectInstance(projectFilePath, evaluator); } // Fall back to looking for a solution if multiple project files are found, or there's no project in the directory. @@ -136,16 +135,16 @@ DependentCommandOptions commandOptions if (!string.IsNullOrEmpty(potentialSln)) { - return GetArbitraryProjectFromSolution(potentialSln, globalProps); + return GetArbitraryProjectFromSolution(potentialSln, evaluator); } } } return null; // If nothing can be found: that's caught by MSBuild XMake::ProcessProjectSwitch -- don't change the behavior by failing here. } - /// An arbitrary existant project in a solution file. Returns null if no projects exist. + /// An arbitrary existent project in a solution file. Returns null if no projects exist. /// Throws exception if two+ projects disagree in PublishRelease, PackRelease, or whatever _propertyToCheck is, and have it defined. - public ProjectInstance? GetArbitraryProjectFromSolution(string slnPath, ReadOnlyDictionary? globalProps) + public DotNetProject? GetArbitraryProjectFromSolution(string slnPath, DotNetProjectEvaluator evaluator) { string slnFullPath = Path.GetFullPath(slnPath); if (!Path.Exists(slnFullPath)) @@ -163,14 +162,14 @@ DependentCommandOptions commandOptions } _isHandlingSolution = true; - List configuredProjects = []; + List configuredProjects = []; HashSet configValues = []; object projectDataLock = new(); if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS), "true", StringComparison.OrdinalIgnoreCase)) { // Evaluate only one project for speed if this environment variable is used. Will break more customers if enabled (adding 8.0 project to SLN with other project TFMs with no Publish or PackRelease.) - return GetSingleProjectFromSolution(sln, slnFullPath, globalProps); + return GetSingleProjectFromSolution(sln, slnFullPath, evaluator); } Parallel.ForEach(sln.SolutionProjects.AsEnumerable(), (project, state) => @@ -181,13 +180,13 @@ DependentCommandOptions commandOptions if (IsUnanalyzableProjectInSolution(project, projectFullPath)) return; - var projectData = TryGetProjectInstance(projectFullPath, globalProps); + var projectData = TryGetProjectInstance(projectFullPath, evaluator); if (projectData == null) { return; } - string pReleasePropertyValue = projectData.GetPropertyValue(_propertyToCheck); + string? pReleasePropertyValue = projectData.GetPropertyValue(_propertyToCheck); if (!string.IsNullOrEmpty(pReleasePropertyValue)) { lock (projectDataLock) @@ -213,9 +212,9 @@ DependentCommandOptions commandOptions /// Returns an arbitrary project for the solution. Relies on the .NET SDK PrepareForPublish or _VerifyPackReleaseConfigurations MSBuild targets to catch conflicting values of a given property, like PublishRelease or PackRelease. /// /// The solution to get an arbitrary project from. - /// The global properties to load into the project. + /// The DotNetProjectEvaluator to use for loading projects. /// null if no project exists in the solution that can be evaluated properly. Else, the first project in the solution that can be. - private ProjectInstance? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, ReadOnlyDictionary? globalProps) + private DotNetProject? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, DotNetProjectEvaluator evaluator) { foreach (var project in sln.SolutionProjects.AsEnumerable()) { @@ -225,7 +224,7 @@ DependentCommandOptions commandOptions if (IsUnanalyzableProjectInSolution(project, projectFullPath)) continue; - var projectData = TryGetProjectInstance(projectFullPath, globalProps); + var projectData = TryGetProjectInstance(projectFullPath, evaluator); if (projectData != null) { return projectData; @@ -246,12 +245,12 @@ private bool IsUnanalyzableProjectInSolution(SolutionProjectModel project, strin return project.TypeId.ToString() == solutionFolderGuid || project.TypeId.ToString() == sharedProjectGuid || !IsValidProjectFilePath(projectFullPath); } - /// Creates a ProjectInstance if the project is valid, elsewise, fails. - private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary? globalProperties) + /// Creates a ProjectInstance if the project is valid, otherwise, fails. + private static DotNetProject? TryGetProjectInstance(string projectPath, DotNetProjectEvaluator evaluator) { try { - return new ProjectInstance(projectPath, globalProperties, "Current"); + return evaluator.LoadProject(projectPath); } catch (Exception e) // Catch failed file access, or invalid project files that cause errors when read into memory, { @@ -271,36 +270,4 @@ private static bool IsValidSlnFilePath(string path) { return File.Exists(path) && (Path.GetExtension(path).Equals(".sln") || Path.GetExtension(path).Equals(".slnx")); } - - /// A case-insensitive dictionary of any properties passed from the user and their values. - private ReadOnlyDictionary? GetUserSpecifiedExplicitMSBuildProperties() => _parseResult.GetValue(CommonOptions.PropertiesOption); - - /// - /// Because command-line options that translate to MSBuild properties aren't in the global arguments from Properties, we need to add the TargetFramework to the collection. - /// The TargetFramework is the only command-line option besides Configuration that could affect the pre-evaluation. - /// This allows the pre-evaluation to correctly deduce its Publish or PackRelease value because it will know the actual TargetFramework being used. - /// - /// The set of MSBuild properties that were specified explicitly like -p:Property=Foo or in other syntax sugars. - /// The same set of global properties for the project, but with the new potential TFM based on -f or --framework. - private ReadOnlyDictionary? InjectTargetFrameworkIntoGlobalProperties(ReadOnlyDictionary? globalProperties) - { - if (globalProperties is null || globalProperties.Count == 0) - { - if (_options.FrameworkOption != null) - { - return new(new Dictionary() { [MSBuildPropertyNames.TARGET_FRAMEWORK] = _options.FrameworkOption }); - } - return null; - } - if (_options.FrameworkOption is null) - { - return globalProperties; - } - - var newDictionary = new Dictionary(globalProperties, StringComparer.OrdinalIgnoreCase); - // Note: dotnet -f FRAMEWORK_1 --property:TargetFramework=FRAMEWORK_2 will use FRAMEWORK_1. - // So we can replace the value in the globals non-dubiously if it exists. - newDictionary[MSBuildPropertyNames.TARGET_FRAMEWORK] = _options.FrameworkOption; - return new(newDictionary); - } } diff --git a/src/Common/EnvironmentVariableNames.cs b/src/Common/EnvironmentVariableNames.cs index 6d73b33ab38f..7b80cd9011e5 100644 --- a/src/Common/EnvironmentVariableNames.cs +++ b/src/Common/EnvironmentVariableNames.cs @@ -29,13 +29,13 @@ static class EnvironmentVariableNames #if NET7_0_OR_GREATER private static readonly Version s_version6_0 = new(6, 0); - public static string? TryGetDotNetRootVariableName(string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, string targetFrameworkVersion) + public static string? TryGetDotNetRootVariableName(string? runtimeIdentifier, string? defaultAppHostRuntimeIdentifier, string? targetFrameworkVersion) => TryGetDotNetRootVariableName(runtimeIdentifier, defaultAppHostRuntimeIdentifier, TryParseTargetFrameworkVersion(targetFrameworkVersion)); - public static string? TryGetDotNetRootVariableName(string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, Version? targetFrameworkVersion) + public static string? TryGetDotNetRootVariableName(string? runtimeIdentifier, string? defaultAppHostRuntimeIdentifier, Version? targetFrameworkVersion) => TryGetDotNetRootVariableNameImpl(runtimeIdentifier, defaultAppHostRuntimeIdentifier, targetFrameworkVersion, RuntimeInformation.ProcessArchitecture, Environment.Is64BitProcess); - internal static string? TryGetDotNetRootVariableNameImpl(string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, Version? targetFrameworkVersion, Architecture currentArchitecture, bool is64bit, bool onlyUseArchSpecific = false) + internal static string? TryGetDotNetRootVariableNameImpl(string? runtimeIdentifier, string? defaultAppHostRuntimeIdentifier, Version? targetFrameworkVersion, Architecture currentArchitecture, bool is64bit, bool onlyUseArchSpecific = false) { // If the app targets the same architecture as SDK is running on or an unknown architecture, set DOTNET_ROOT, DOTNET_ROOT(x86) for 32-bit, DOTNET_ROOT_arch for TFM 6+. // If the app targets different architecture from the SDK, do not set DOTNET_ROOT. @@ -53,13 +53,18 @@ static class EnvironmentVariableNames return null; } - internal static string? TryGetDotNetRootArchVariableName(string runtimeIdentifier, string defaultAppHostRuntimeIdentifier) + internal static string? TryGetDotNetRootArchVariableName(string? runtimeIdentifier, string? defaultAppHostRuntimeIdentifier) => TryGetDotNetRootVariableNameImpl(runtimeIdentifier, defaultAppHostRuntimeIdentifier, null, RuntimeInformation.ProcessArchitecture, Environment.Is64BitProcess, onlyUseArchSpecific: true); - internal static bool TryParseArchitecture(string runtimeIdentifier, out Architecture architecture) + internal static bool TryParseArchitecture(string? runtimeIdentifier, out Architecture architecture) { // RID is [os].[version]-[architecture]-[additional qualifiers] // See https://learn.microsoft.com/en-us/dotnet/core/rid-catalog + if (runtimeIdentifier == null) + { + architecture = default; + return false; + } int archStart = runtimeIdentifier.IndexOf('-') + 1; if (archStart <= 0) @@ -74,7 +79,7 @@ internal static bool TryParseArchitecture(string runtimeIdentifier, out Architec return Enum.TryParse(span, ignoreCase: true, out architecture); } - public static Version? TryParseTargetFrameworkVersion(string targetFrameworkVersion) + public static Version? TryParseTargetFrameworkVersion(string? targetFrameworkVersion) { // TargetFrameworkVersion appears as "vX.Y" in msbuild. Ignore the leading 'v'. return !string.IsNullOrEmpty(targetFrameworkVersion) && Version.TryParse(targetFrameworkVersion.Substring(1), out var version) ? version : null; diff --git a/test/Msbuild.Tests.Utilities/ProjectRootElementExtensions.cs b/test/Msbuild.Tests.Utilities/ProjectRootElementExtensions.cs index 0295b02a35d9..2330340fce31 100644 --- a/test/Msbuild.Tests.Utilities/ProjectRootElementExtensions.cs +++ b/test/Msbuild.Tests.Utilities/ProjectRootElementExtensions.cs @@ -65,5 +65,25 @@ public static int NumberOfProjectReferencesWithIncludeContaining( { return root.ItemsWithIncludeContaining("ProjectReference", includePattern).Count(); } + + public static ISet ConditionChain(this ProjectElement projectElement) + { + var conditionChainSet = new HashSet(); + + if (!string.IsNullOrEmpty(projectElement.Condition)) + { + conditionChainSet.Add(projectElement.Condition); + } + + foreach (var parent in projectElement.AllParents) + { + if (!string.IsNullOrEmpty(parent.Condition)) + { + conditionChainSet.Add(parent.Condition); + } + } + + return conditionChainSet; + } } }