Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,7 @@ public static string MSBuildVersion
// True if, given current state of the class, MSBuild would be executed in its own process.
public bool ExecuteMSBuildOutOfProc => _forwardingApp != null;

private readonly Dictionary<string, string> _msbuildRequiredEnvironmentVariables =
new()
{
{ "MSBuildExtensionsPath", MSBuildExtensionsPathTestHook ?? AppContext.BaseDirectory },
{ "MSBuildSDKsPath", GetMSBuildSDKsPath() },
{ "DOTNET_HOST_PATH", GetDotnetPath() },
};
private readonly Dictionary<string, string> _msbuildRequiredEnvironmentVariables = GetMSBuildRequiredEnvironmentVariables();

private readonly List<string> _msbuildRequiredParameters =
[ "-maxcpucount", "-verbosity:m" ];
Expand Down Expand Up @@ -205,6 +199,16 @@ private static string GetDotnetPath()
return new Muxer().MuxerPath;
}

internal static Dictionary<string, string> GetMSBuildRequiredEnvironmentVariables()
{
return new()
{
{ "MSBuildExtensionsPath", MSBuildExtensionsPathTestHook ?? AppContext.BaseDirectory },
{ "MSBuildSDKsPath", GetMSBuildSDKsPath() },
{ "DOTNET_HOST_PATH", GetDotnetPath() },
};
}

private static bool IsRestoreSources(string arg)
{
return arg.StartsWith("/p:RestoreSources=", StringComparison.OrdinalIgnoreCase) ||
Expand Down
193 changes: 193 additions & 0 deletions src/Cli/dotnet/commands/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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.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;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Tools;

/// <summary>
/// Used to build a virtual project file in memory to support <c>dotnet run file.cs</c>.
/// </summary>
internal sealed class VirtualProjectBuildingCommand
{
public Dictionary<string, string> GlobalProperties { get; } = new(StringComparer.OrdinalIgnoreCase);
public required string EntryPointFileFullPath { get; init; }

public int Execute(string[] binaryLoggerArgs, LoggerVerbosity verbosity)
{
var binaryLogger = GetBinaryLogger(binaryLoggerArgs);
var consoleLogger = new ConsoleLogger(verbosity);
Dictionary<string, string?> savedEnvironmentVariables = new();
try
{
// Set environment variables.
foreach (var (key, value) in MSBuildForwardingAppWithoutLogging.GetMSBuildRequiredEnvironmentVariables())
{
savedEnvironmentVariables[key] = Environment.GetEnvironmentVariable(key);
Environment.SetEnvironmentVariable(key, value);
}

// Setup MSBuild.
ReadOnlySpan<ILogger> binaryLoggers = binaryLogger is null ? [] : [binaryLogger];
var projectCollection = new ProjectCollection(
GlobalProperties,
[.. binaryLoggers, consoleLogger],
ToolsetDefinitionLocations.Default);
var parameters = new BuildParameters(projectCollection)
{
Loggers = projectCollection.Loggers,
LogTaskInputs = binaryLogger is not null,
};
BuildManager.DefaultBuildManager.BeginBuild(parameters);

// Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`).
// See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838.
var restoreRequest = new BuildRequestData(
CreateProjectInstance(projectCollection, addGlobalProperties: static (globalProperties) =>
{
globalProperties["MSBuildRestoreSessionId"] = Guid.NewGuid().ToString("D");
globalProperties["MSBuildIsRestoring"] = bool.TrueString;
}),
targetsToBuild: ["Restore"],
hostServices: null,
BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports | BuildRequestDataFlags.FailOnUnresolvedSdk);
var restoreResult = BuildManager.DefaultBuildManager.BuildRequest(restoreRequest);
if (restoreResult.OverallResult != BuildResultCode.Success)
{
return 1;
}

// Then do a build.
var buildRequest = new BuildRequestData(
CreateProjectInstance(projectCollection),
targetsToBuild: ["Build"]);
var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest);
if (buildResult.OverallResult != BuildResultCode.Success)
{
return 1;
}

BuildManager.DefaultBuildManager.EndBuild();
return 0;
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
return 1;
}
finally
{
foreach (var (key, value) in savedEnvironmentVariables)
{
Environment.SetEnvironmentVariable(key, value);
}

binaryLogger?.Shutdown();
consoleLogger.Shutdown();
}

static ILogger? GetBinaryLogger(string[] args)
{
for (int i = args.Length - 1; i >= 0; i--)
{
var arg = args[i];
if (arg.StartsWith("/bl:") || arg.Equals("/bl")
|| arg.StartsWith("--binaryLogger:") || arg.Equals("--binaryLogger")
|| arg.StartsWith("-bl:") || arg.Equals("-bl"))
{
return new BinaryLogger
{
Parameters = arg.IndexOf(':') is >= 0 and var index
? arg[(index + 1)..]
: "msbuild.binlog",
};
}
}

return null;
}
}

public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection)
{
return CreateProjectInstance(projectCollection, addGlobalProperties: null);
}

private ProjectInstance CreateProjectInstance(
ProjectCollection projectCollection,
Action<IDictionary<string, string>>? addGlobalProperties = null)
{
var projectRoot = CreateProjectRootElement(projectCollection);

var globalProperties = projectCollection.GlobalProperties;
if (addGlobalProperties is not null)
{
globalProperties = new Dictionary<string, string>(projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase);
addGlobalProperties(globalProperties);
}

return ProjectInstance.FromProjectRootElement(projectRoot, new ProjectOptions
{
GlobalProperties = globalProperties,
});
}

private ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
{
var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
var projectFileText = """
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

<!-- Override targets which don't work with project files that are not present on disk. -->

<Target Name="_FilterRestoreGraphProjectInputItems"
DependsOnTargets="_LoadRestoreGraphEntryPoints"
Returns="@(FilteredRestoreGraphProjectInputItems)">
<ItemGroup>
<FilteredRestoreGraphProjectInputItems Include="@(RestoreGraphProjectInputItems)" />
</ItemGroup>
</Target>

<Target Name="_GetAllRestoreProjectPathItems"
DependsOnTargets="_FilterRestoreGraphProjectInputItems"
Returns="@(_RestoreProjectPathItems)">
<ItemGroup>
<_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" />
</ItemGroup>
</Target>

<Target Name="_GenerateRestoreGraph"
DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GetAllRestoreProjectPathItems;_GenerateRestoreGraphProjectEntry;_GenerateProjectRestoreGraph"
Returns="@(_RestoreGraphEntry)">
<!-- Output from dependency _GenerateRestoreGraphProjectEntry and _GenerateProjectRestoreGraph -->
</Target>
</Project>
""";
ProjectRootElement projectRoot;
using (var xmlReader = XmlReader.Create(new StringReader(projectFileText)))
{
projectRoot = ProjectRootElement.Create(xmlReader, projectCollection);
}
projectRoot.FullPath = projectFileFullPath;
return projectRoot;
}
}
6 changes: 6 additions & 0 deletions src/Cli/dotnet/commands/dotnet-run/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,10 @@ Make the profile names distinct.</value>
<data name="LaunchProfileDoesNotExist" xml:space="preserve">
<value>A launch profile with the name '{0}' doesn't exist.</value>
</data>
<data name="NoTopLevelStatements" xml:space="preserve">
<value>Cannot run a file without top-level statements and without a project: '{0}'</value>
</data>
<data name="RunFileUnsupportedSwitch" xml:space="preserve">
<value>The option '{0}' is not supported when running a file without a project: '{1}'</value>
</data>
</root>
Loading