/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/VirtualProjectBuilder.cs
985 строк
38 KB
Jan Jones
Allow MSBuildWorkspace to open file-based apps (#84139)
30 июл 2026, 11:03
Не верифицирован
30 июл 2026, 11:03
7e7ebef
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Threading.Tasks; using System.Xml; using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Utilities; namespace Microsoft.DotNet.FileBasedPrograms; #if FILE_BASED_PROGRAMS_PUBLIC public #else internal #endif sealed class VirtualProjectBuilder { internal readonly record struct ExplicitProjectItem(string ItemType, string Include); internal const string FromIncludeDirectiveMetadataName = "FileBasedProgramsFromIncludeDirective"; internal const string FromRefDirectiveMetadataName = "FileBasedProgramsFromRefDirective"; private readonly IBuildService _buildService; private readonly string? _targetFramework; private (ImmutableArray<CSharpDirective> Original, ImmutableArray<CSharpDirective> Evaluated)? _evaluatedDirectives; internal string EntryPointFileFullPath { get; } internal SourceFile EntryPointSourceFile { get { if (field == default) { field = SourceFile.Load(EntryPointFileFullPath); } return field; } } internal string ArtifactsPath => field ??= GetArtifactsPath(EntryPointFileFullPath); internal string[]? RequestedTargets { get; } internal VirtualProjectBuilder( IBuildService buildService, string entryPointFileFullPath, string? targetFramework, string[]? requestedTargets = null, string? artifactsPath = null, SourceText? sourceText = null) { Debug.Assert(ExternalHelpers.IsPathFullyQualified(entryPointFileFullPath)); _buildService = buildService; EntryPointFileFullPath = entryPointFileFullPath; RequestedTargets = requestedTargets; ArtifactsPath = artifactsPath; _targetFramework = targetFramework; if (sourceText != null) { EntryPointSourceFile = new SourceFile(entryPointFileFullPath, sourceText); } } /// <remarks> /// Kept in sync with the default <c>dotnet new console</c> project file (enforced by <c>DotnetProjectConvertTests.SameAsTemplate</c>). /// </remarks> internal static IEnumerable<(string name, string value)> GetDefaultProperties(string? targetFramework) { yield return ("OutputType", "Exe"); if (targetFramework != null) yield return ("TargetFramework", targetFramework); yield return ("ImplicitUsings", "enable"); yield return ("Nullable", "enable"); yield return ("PublishAot", "true"); yield return ("PackAsTool", "true"); } internal static IEnumerable<KeyValuePair<string, string>> GetGlobalBuildProperties() => [ // See https://github.com/dotnet/msbuild/blob/main/documentation/specs/build-nonexistent-projects-by-default.md. new KeyValuePair<string, string>("_BuildNonexistentProjectsByDefault", bool.TrueString), new KeyValuePair<string, string>("RestoreUseSkipNonexistentTargets", bool.FalseString), ]; internal static string GetArtifactsPath(string entryPointFileFullPath, string? dotNetSubdirectory = null) { // Include entry point file name so the directory name is not completely opaque. string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath); string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath); string directoryName = $"{fileName}-{hash}"; return GetTempSubpath(name: directoryName, dotNetSubdirectory: dotNetSubdirectory); } private const string CsprojExtension = ".csproj"; public static string GetVirtualProjectPath(string entryPointFilePath) => entryPointFilePath + CsprojExtension; public static bool TryGetEntryPointFilePathFromVirtualProjectPath(string projectPath, [NotNullWhen(returnValue: true)] out string? entryPointFilePath) { if (projectPath.EndsWith(CsprojExtension, StringComparison.OrdinalIgnoreCase)) { entryPointFilePath = projectPath[..^CsprojExtension.Length]; if (IsValidEntryPointPath(entryPointFilePath)) { return true; } } entryPointFilePath = null; return false; } /// <summary> /// Parses a source file to extract property value from directives. /// </summary> /// <returns>Array of frameworks if TargetFrameworks is specified, or empty otherwise</returns> public static string? GetPropertyFromSourceFile(string sourceFilePath, string propertyName) { var sourceFile = SourceFile.Load(sourceFilePath); var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, ErrorReporters.IgnoringReporter); // Return the first value. Conflicting duplicate directives are not supported. return directives.OfType<CSharpDirective.Property>() .FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase))?.Value; } /// <summary> /// Obtains a temporary subdirectory for file-based app artifacts, e.g., <c>/tmp/dotnet/runfile/</c>. /// </summary> internal static string GetTempSubdirectory(string? dotNetSubdirectory = null) { // We want a location where permissions are expected to be restricted to the current user. string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.GetTempPath() : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); if (string.IsNullOrEmpty(directory)) { throw new InvalidOperationException(FileBasedProgramsResources.EmptyTempPath); } return Path.Combine(directory, "dotnet", dotNetSubdirectory ?? "runfile"); } /// <summary> /// Obtains a specific temporary path in a subdirectory for file-based app artifacts, e.g., <c>/tmp/dotnet/runfile/{name}</c>. /// </summary> internal static string GetTempSubpath(string name, string? dotNetSubdirectory = null) { return Path.Combine(GetTempSubdirectory(dotNetSubdirectory), name); } public static bool IsValidEntryPointPath(string entryPointFilePath) { if (!File.Exists(entryPointFilePath)) { return false; } if (entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) { return true; } // Check if the first two characters are #! try { using var stream = File.OpenRead(entryPointFilePath); int first = stream.ReadByte(); int second = stream.ReadByte(); return first == '#' && second == '!'; } catch { return false; } } /// <summary> /// Evaluates <paramref name="directives"/> against a <paramref name="project"/> and the file system. /// </summary> /// <remarks> /// All directives that need some other evaluation (described below) are expanded as MSBuild expressions /// (i.e., <c>$()</c> and <c>@()</c> are substituted with property and item values, etc.). /// <para/> /// <c>#:project</c> directives are resolved to full project file paths /// (e.g., if the evaluated value is a directory, finds a project in that directory). /// <para/> /// <c>#:include</c>/<c>#:exclude</c> have their <see cref="CSharpDirective.IncludeOrExclude.ItemType"/> determined /// and relative paths resolved relative to their containing file. /// </remarks> private async ValueTask<ImmutableArray<CSharpDirective>> EvaluateDirectivesAsync( IProjectInstance project, ImmutableArray<CSharpDirective> directives, ErrorReporter reportError) { if (!directives.Any(static d => d is CSharpDirective.Project or CSharpDirective.IncludeOrExclude or CSharpDirective.Ref)) { return directives; } var builder = ImmutableArray.CreateBuilder<CSharpDirective>(directives.Length); ImmutableArray<(string Extension, string ItemType)> mapping = default; foreach (var directive in directives) { switch (directive) { case CSharpDirective.Project projectDirective: projectDirective = projectDirective.WithName(await project.ExpandStringAsync(projectDirective.Name).ConfigureAwait(false), CSharpDirective.Project.NameKind.Expanded); projectDirective = projectDirective.EnsureProjectFilePath(reportError); builder.Add(projectDirective); break; case CSharpDirective.Ref refDirective: refDirective = refDirective.WithName(await project.ExpandStringAsync(refDirective.Name).ConfigureAwait(false), CSharpDirective.Ref.NameKind.Expanded); refDirective = refDirective.EnsureResolvedPath(reportError); builder.Add(refDirective); break; case CSharpDirective.IncludeOrExclude includeOrExcludeDirective: var expandedPath = await project.ExpandStringAsync(includeOrExcludeDirective.Name).ConfigureAwait(false); var fullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(includeOrExcludeDirective.Info.SourceFile.Path)!, expandedPath)); includeOrExcludeDirective = includeOrExcludeDirective.WithName(fullPath); if (mapping.IsDefault) { mapping = await GetItemMappingAsync(project, reportError).ConfigureAwait(false); } includeOrExcludeDirective = includeOrExcludeDirective.WithDeterminedItemType(reportError, mapping); builder.Add(includeOrExcludeDirective); break; default: builder.Add(directive); break; } } return builder.DrainToImmutable(); } internal async ValueTask<ImmutableArray<(string Extension, string ItemType)>> GetItemMappingAsync(IProjectInstance project, ErrorReporter reportError) { return CSharpDirective.IncludeOrExclude.ParseMapping( await project.GetPropertyValueAsync(CSharpDirective.IncludeOrExclude.MappingPropertyName).ConfigureAwait(false), EntryPointSourceFile, reportError); } public static async ValueTask<IProjectInstance> CreateProjectInstanceAsync( IBuildService buildService, string entryPointFilePath, string targetFramework, IProjectCollection projectCollection, Action<string, int, string> errorReporter) { var builder = new VirtualProjectBuilder(buildService, entryPointFilePath, targetFramework); var result = await builder.CreateProjectInstanceAsync( projectCollection, (text, path, textSpan, message, _) => errorReporter(path, text.Lines.GetLinePositionSpan(textSpan).Start.Line + 1, message)).ConfigureAwait(false); return result.Project; } internal readonly struct Result( IProjectInstance project, IProjectRootElement projectRootElement, ImmutableArray<CSharpDirective> evaluatedDirectives) { public IProjectInstance Project { get; } = project; public IProjectRootElement ProjectRootElement { get; } = projectRootElement; public ImmutableArray<CSharpDirective> EvaluatedDirectives { get; } = evaluatedDirectives; } internal async ValueTask<Result> CreateProjectInstanceAsync( IProjectCollection projectCollection, ErrorReporter reportError, ImmutableArray<CSharpDirective> directives = default, IDictionary<string, string>? additionalGlobalProperties = null, bool validateAllDirectives = false, HashSet<string>? processedRefFiles = null) { IProjectInstance project; IProjectRootElement projectRootElement; ImmutableArray<CSharpDirective> evaluatedDirectives; var directivesOriginal = directives; if (directives.IsDefault) { directives = FileLevelDirectiveHelpers.FindDirectives(EntryPointSourceFile, validateAllDirectives, reportError, checkDuplicates: false); } (string ProjectFileText, IProjectInstance ProjectInstance, IProjectRootElement ProjectRootElement)? lastProject = null; // If we evaluated directives previously (e.g., during restore), reuse them. // We don't use the additional properties from `addGlobalProperties` // during directive evaluation anyway, so the directives can be reused safely. if (_evaluatedDirectives is { } cached && cached.Original == directivesOriginal) { evaluatedDirectives = cached.Evaluated; (project, projectRootElement) = await CreateProjectInstanceNoEvaluation( projectCollection, evaluatedDirectives, additionalGlobalProperties).ConfigureAwait(false); } else { var entryPointDirectory = Path.GetDirectoryName(EntryPointFileFullPath)!; var seenFiles = new HashSet<string>(StringComparer.Ordinal) { EntryPointFileFullPath }; var filesToProcess = new Queue<string>(); var evaluatedDirectiveBuilder = ImmutableArray.CreateBuilder<CSharpDirective>(); var deduplicator = new DirectiveDeduplicator(); do { var directivesForEvaluation = DeduplicateSdkDirectives(directives); // Create a project with properties from #:property directives so they can be expanded inside EvaluateDirectives. (project, projectRootElement) = await CreateProjectInstanceNoEvaluation( projectCollection, [.. evaluatedDirectiveBuilder, .. directivesForEvaluation], additionalGlobalProperties).ConfigureAwait(false); // Evaluate directives, e.g., determine item types for #:include/#:exclude from their file extension. var fileEvaluatedDirectives = await EvaluateDirectivesAsync(project, directivesForEvaluation, reportError).ConfigureAwait(false); // Detect duplicate directives across all files on evaluated directives. EvaluateDirectives only expands // #:project, #:ref, #:include, and #:exclude; #:property and #:package values are still unevaluated here. var deduplicatedFileEvaluatedDirectiveBuilder = ImmutableArray.CreateBuilder<CSharpDirective>(fileEvaluatedDirectives.Length); foreach (var directive in fileEvaluatedDirectives) { if (directive is CSharpDirective.Sdk) { deduplicatedFileEvaluatedDirectiveBuilder.Add(directive); continue; } if (directive is CSharpDirective.Named named) { deduplicator.CheckDirective(named, reportError, out bool shouldKeep); if (!shouldKeep) { continue; } } deduplicatedFileEvaluatedDirectiveBuilder.Add(directive); } fileEvaluatedDirectives = deduplicatedFileEvaluatedDirectiveBuilder.DrainToImmutable(); evaluatedDirectiveBuilder.AddRange(fileEvaluatedDirectives); if (fileEvaluatedDirectives != directives) { // This project will contain items from #:include/#:exclude directives which we will traverse recursively. (project, projectRootElement) = await CreateProjectInstanceNoEvaluation( projectCollection, evaluatedDirectiveBuilder.ToImmutable(), additionalGlobalProperties).ConfigureAwait(false); } var compileItems = await project.GetItemMetadataValuesAsync("Compile", ["FullPath"]).ConfigureAwait(false); foreach (var compileItem in compileItems) { Debug.Assert(compileItem.Length == 1); var fullPath = compileItem[0]; var compilePath = Path.GetFullPath(Path.Combine( entryPointDirectory, fullPath)); if (seenFiles.Add(compilePath)) { filesToProcess.Enqueue(compilePath); } } } while (TryGetNextFileToProcess()); evaluatedDirectives = evaluatedDirectiveBuilder.ToImmutable(); _evaluatedDirectives = (directivesOriginal, evaluatedDirectives); bool TryGetNextFileToProcess() { while (filesToProcess.Count != 0) { var filePath = filesToProcess.Dequeue(); if (!File.Exists(filePath)) { reportError(EntryPointSourceFile.Text, EntryPointSourceFile.Path, default, string.Format(FileBasedProgramsResources.IncludedFileNotFound, filePath)); continue; } var sourceFile = SourceFile.Load(filePath); directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, validateAllDirectives, reportError, checkDuplicates: false); return true; } return false; } // #:sdk directives become Sdk.props/Sdk.targets imports when creating the temporary project used for // directive evaluation, so identical duplicates must be removed before that project is created. ImmutableArray<CSharpDirective> DeduplicateSdkDirectives(ImmutableArray<CSharpDirective> directives) { if (!directives.Any(static directive => directive is CSharpDirective.Sdk)) { return directives; } var builder = ImmutableArray.CreateBuilder<CSharpDirective>(directives.Length); var changed = false; foreach (var directive in directives) { if (directive is CSharpDirective.Sdk sdk) { deduplicator.CheckDirective(sdk, reportError, out bool shouldKeep); if (!shouldKeep) { changed = true; continue; } } builder.Add(directive); } return changed ? builder.DrainToImmutable() : directives; } } await CheckDirectivesAsync(project, evaluatedDirectives, reportError).ConfigureAwait(false); await CreateReferencedVirtualProjectsAsync(projectCollection, evaluatedDirectives, reportError, validateAllDirectives, processedRefFiles).ConfigureAwait(false); return new Result(project, projectRootElement, evaluatedDirectives); async ValueTask<(IProjectInstance, IProjectRootElement)> CreateProjectInstanceNoEvaluation( IProjectCollection projectCollection, ImmutableArray<CSharpDirective> directives, IDictionary<string, string>? additionalGlobalProperties = null) { var projectFileWriter = new StringWriter(); WriteProjectFile( projectFileWriter, directives, GetDefaultProperties(_targetFramework), isVirtualProject: true, entryPointFilePath: EntryPointFileFullPath, artifactsPath: ArtifactsPath, includeRuntimeConfigInformation: RequestedTargets?.Any(static t => t is "Publish" or "Pack") != true); var projectFileText = projectFileWriter.ToString(); // If nothing changed, reuse the previous project instance to avoid unnecessary re-evaluations. if (lastProject is { } cachedProject && cachedProject.ProjectFileText == projectFileText) { return (cachedProject.ProjectInstance, cachedProject.ProjectRootElement); } var projectRoot = CreateProjectRootElement(projectFileText, projectCollection); var project = await _buildService.CreateProjectInstanceFromProjectRootElementAsync(projectRoot, projectCollection, additionalGlobalProperties).ConfigureAwait(false); lastProject = (projectFileText, project, projectRoot); return (project, projectRoot); IProjectRootElement CreateProjectRootElement(string projectFileText, IProjectCollection projectCollection) { using var reader = new StringReader(projectFileText); using var xmlReader = XmlReader.Create(reader); var projectRoot = _buildService.CreateProjectRootElement(xmlReader, projectCollection, EntryPointFileFullPath); projectRoot.FullPath = GetVirtualProjectPath(EntryPointFileFullPath); return projectRoot; } } } /// <summary> /// Recursively creates virtual <see cref="IProjectRootElement"/>s for all <c>#:ref</c> directives /// so MSBuild can resolve <c><ProjectReference></c> items to them. /// </summary> private async ValueTask CreateReferencedVirtualProjectsAsync( IProjectCollection projectCollection, ImmutableArray<CSharpDirective> directives, ErrorReporter reportError, bool validateAllDirectives, HashSet<string>? processedFiles) { if (!directives.Any(static d => d is CSharpDirective.Ref)) { return; } processedFiles ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase); processedFiles.Add(EntryPointFileFullPath); foreach (var refDirective in directives.OfType<CSharpDirective.Ref>()) { Debug.Assert(refDirective.ResolvedPath is not null); if (refDirective.ResolvedPath is not { } resolvedPath) { continue; } if (!processedFiles.Add(resolvedPath)) { continue; } var refBuilder = new VirtualProjectBuilder(_buildService, resolvedPath, _targetFramework); await refBuilder.CreateProjectInstanceAsync( projectCollection, reportError, validateAllDirectives: validateAllDirectives, processedRefFiles: processedFiles).ConfigureAwait(false); } } private async ValueTask CheckDirectivesAsync( IProjectInstance project, ImmutableArray<CSharpDirective> directives, ErrorReporter reportError) { var refEnabled = new StrongBox<bool?>(); foreach (var directive in directives) { if (directive is CSharpDirective.Ref) { await CheckFlagEnabledAsync(refEnabled, CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, directive).ConfigureAwait(false); } } async ValueTask CheckFlagEnabledAsync(StrongBox<bool?> flag, string flagName, CSharpDirective directive) { bool value = flag.Value ??= MSBuildUtilities.ConvertStringToBool(await project.GetPropertyValueAsync(flagName).ConfigureAwait(false)); if (!value) { reportError( directive.Info.SourceFile.Text, directive.Info.SourceFile.Path, directive.Info.Span, string.Format(FileBasedProgramsResources.ExperimentalFeatureDisabled, flagName)); } } } internal static void WriteProjectFile( TextWriter writer, ImmutableArray<CSharpDirective> directives, IEnumerable<(string name, string value)> defaultProperties, bool isVirtualProject, string? entryPointFilePath = null, string? artifactsPath = null, bool includeRuntimeConfigInformation = true, string? userSecretsId = null, ImmutableArray<ExplicitProjectItem> explicitProjectItems = default) { Debug.Assert(userSecretsId == null || !isVirtualProject); int processedDirectives = 0; var sdkDirectives = directives.OfType<CSharpDirective.Sdk>(); var propertyDirectives = directives.OfType<CSharpDirective.Property>(); var packageDirectives = directives.OfType<CSharpDirective.Package>(); var projectDirectives = directives.OfType<CSharpDirective.Project>(); var refDirectives = directives.OfType<CSharpDirective.Ref>(); var includeOrExcludeDirectives = directives.OfType<CSharpDirective.IncludeOrExclude>().ToArray(); const string defaultSdkName = "Microsoft.NET.Sdk"; string firstSdkName; string? firstSdkVersion; if (sdkDirectives.FirstOrDefault() is { } firstSdk) { firstSdkName = firstSdk.Name; firstSdkVersion = firstSdk.Version; processedDirectives++; } else { firstSdkName = defaultSdkName; firstSdkVersion = null; } if (isVirtualProject) { Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath)); Debug.Assert(entryPointFilePath is not null); // Note that ArtifactsPath needs to be specified before Sdk.props // (usually it's recommended to specify it in Directory.Build.props // but importing Sdk.props manually afterwards also works). writer.WriteLine($""" <Project> <PropertyGroup> <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths> <ArtifactsPath>{EscapeValue(artifactsPath)}</ArtifactsPath> <AssemblyName>{EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))}</AssemblyName> <RootNamespace>$(AssemblyName)</RootNamespace> <PublishDir>artifacts/$(AssemblyName)</PublishDir> <PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath> <FileBasedProgram>true</FileBasedProgram> <EntryPointFilePath>{EscapeValue(entryPointFilePath)}</EntryPointFilePath> <FileBasedProgramsItemMapping>{CSharpDirective.IncludeOrExclude.DefaultMappingString}</FileBasedProgramsItemMapping> <EnableDefaultCompileItems>false</EnableDefaultCompileItems> <DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder> """); // Only set these to false when using the default SDK with no additional SDKs // to avoid including .resx and other files that are typically not expected in simple file-based apps. // When other SDKs are used (e.g., Microsoft.NET.Sdk.Web), keep the default behavior. bool usingOnlyDefaultSdk = firstSdkName == defaultSdkName && sdkDirectives.Count() <= 1; if (usingOnlyDefaultSdk) { writer.WriteLine(""" <EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems> <EnableDefaultNoneItems>false</EnableDefaultNoneItems> """); } // Write default properties before importing SDKs so they can be overridden by SDKs // (and implicit build files which are imported by the default .NET SDK). foreach (var (name, value) in defaultProperties) { writer.WriteLine($""" <{name}>{EscapeValue(value)}</{name}> """); } writer.WriteLine($""" </PropertyGroup> <ItemGroup> <Clean Include="{EscapeValue(artifactsPath)}/*" /> </ItemGroup> """); if (firstSdkVersion is null) { writer.WriteLine($""" <Import Project="Sdk.props" Sdk="{EscapeValue(firstSdkName)}" /> """); } else { writer.WriteLine($""" <Import Project="Sdk.props" Sdk="{EscapeValue(firstSdkName)}" Version="{EscapeValue(firstSdkVersion)}" /> """); } } else { string slashDelimited = firstSdkVersion is null ? firstSdkName : $"{firstSdkName}/{firstSdkVersion}"; writer.WriteLine($""" <Project Sdk="{EscapeValue(slashDelimited)}"> """); } foreach (var sdk in sdkDirectives.Skip(1)) { if (isVirtualProject) { WriteImport(writer, "Sdk.props", sdk); } else if (sdk.Version is null) { writer.WriteLine($""" <Sdk Name="{EscapeValue(sdk.Name)}" /> """); } else { writer.WriteLine($""" <Sdk Name="{EscapeValue(sdk.Name)}" Version="{EscapeValue(sdk.Version)}" /> """); } processedDirectives++; } if (isVirtualProject || processedDirectives > 1) { writer.WriteLine(); } // Write default and custom properties. { writer.WriteLine(""" <PropertyGroup> """); // First write the default properties except those specified by the user. if (!isVirtualProject) { var customPropertyNames = propertyDirectives .Select(static d => d.Name) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var (name, value) in defaultProperties) { if (!customPropertyNames.Contains(name)) { writer.WriteLine($""" <{name}>{EscapeValue(value)}</{name}> """); } } if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId")) { writer.WriteLine($""" <UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId> """); } } // Some hosts (like MSBuildWorkspace) don't provide a default TargetFramework // and instead want to use the TargetFramework that's default corresponding to the imported SDK props. else if (!defaultProperties.Any(p => p.name == "TargetFramework")) { writer.WriteLine(""" <TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework> """); } // Write custom properties. foreach (var property in propertyDirectives) { writer.WriteLine($""" <{property.Name}>{EscapeValue(property.Value)}</{property.Name}> """); processedDirectives++; } // Write virtual-only properties which cannot be overridden. if (isVirtualProject) { writer.WriteLine(""" <RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation> <Features>$(Features);FileBasedProgram</Features> """); } writer.WriteLine(""" </PropertyGroup> """); } if (!isVirtualProject) { // In the real project, files are included by the conversion copying them to the output directory, // hence we don't need to transfer the #:include/#:exclude directives over by default. processedDirectives += includeOrExcludeDirectives.Length; } else if (includeOrExcludeDirectives.Length > 0) { writer.WriteLine(""" <ItemGroup> """); foreach (var includeOrExclude in includeOrExcludeDirectives) { processedDirectives++; var itemType = includeOrExclude.ItemType; if (itemType == null) { // Before directives are evaluated, the item type is null. // We still need to create the project (so that we can evaluate $() properties), // but we can skip the items. continue; } if (includeOrExclude.Kind == CSharpDirective.IncludeOrExcludeKind.Include) { writer.WriteLine($""" <{itemType} Include="{EscapeValue(includeOrExclude.Name)}" {FromIncludeDirectiveMetadataName}="true" /> """); } else { writer.WriteLine($""" <{itemType} Remove="{EscapeValue(includeOrExclude.Name)}" /> """); } } writer.WriteLine(""" </ItemGroup> """); } if (!explicitProjectItems.IsDefaultOrEmpty) { writer.WriteLine(""" <ItemGroup> """); foreach (var (itemType, include) in explicitProjectItems) { writer.WriteLine($""" <{itemType} Include="{EscapeValue(include)}" /> """); } writer.WriteLine(""" </ItemGroup> """); } if (packageDirectives.Any()) { writer.WriteLine(""" <ItemGroup> """); foreach (var package in packageDirectives) { if (package.Version is null) { writer.WriteLine($""" <PackageReference Include="{EscapeValue(package.Name)}" /> """); } else { writer.WriteLine($""" <PackageReference Include="{EscapeValue(package.Name)}" Version="{EscapeValue(package.Version)}" /> """); } processedDirectives++; } writer.WriteLine(""" </ItemGroup> """); } if (projectDirectives.Any() || refDirectives.Any()) { writer.WriteLine(""" <ItemGroup> """); foreach (var projectReference in projectDirectives) { writer.WriteLine($""" <ProjectReference Include="{EscapeValue(projectReference.Name)}" /> """); processedDirectives++; } foreach (var refDirective in refDirectives) { if (refDirective.ResolvedPath is not null) { var virtualProjectPath = GetVirtualProjectPath(refDirective.ResolvedPath); writer.WriteLine($""" <ProjectReference Include="{EscapeValue(virtualProjectPath)}" {FromRefDirectiveMetadataName}="{EscapeValue(refDirective.ResolvedPath)}" /> """); } processedDirectives++; } writer.WriteLine(""" </ItemGroup> """); } Debug.Assert(processedDirectives + directives.OfType<CSharpDirective.Shebang>().Count() == directives.Length); if (isVirtualProject) { Debug.Assert(entryPointFilePath is not null); // We Exclude existing Compile items (which could be added e.g. // in Microsoft.NET.Sdk.DefaultItems.props when user sets EnableDefaultCompileItems=true, // or above via #:include/#:exclude directives). writer.WriteLine($""" <ItemGroup> <Compile Include="{EscapeValue(entryPointFilePath)}" Exclude="@(Compile)" /> </ItemGroup> """); if (includeRuntimeConfigInformation) { var entryPointDirectory = Path.GetDirectoryName(entryPointFilePath) ?? ""; writer.WriteLine($""" <ItemGroup> <RuntimeHostConfigurationOption Include="EntryPointFilePath" Value="{EscapeValue(entryPointFilePath)}" /> <RuntimeHostConfigurationOption Include="EntryPointFileDirectoryPath" Value="{EscapeValue(entryPointDirectory)}" /> </ItemGroup> """); } foreach (var sdk in sdkDirectives) { WriteImport(writer, "Sdk.targets", sdk); } if (!sdkDirectives.Any()) { Debug.Assert(firstSdkName == defaultSdkName && firstSdkVersion == null); writer.WriteLine($""" <Import Project="Sdk.targets" Sdk="{defaultSdkName}" /> """); } writer.WriteLine(); } writer.WriteLine(""" </Project> """); static string EscapeValue(string value) => SecurityElement.Escape(value); static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk sdk) { if (sdk.Version is null) { writer.WriteLine($""" <Import Project="{EscapeValue(project)}" Sdk="{EscapeValue(sdk.Name)}" /> """); } else { writer.WriteLine($""" <Import Project="{EscapeValue(project)}" Sdk="{EscapeValue(sdk.Name)}" Version="{EscapeValue(sdk.Version)}" /> """); } } } }