// 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)}" />
""");
}
}
}
}