Skip to content

Add basic support for 'dotnet run file.cs' #46915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -36,13 +36,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 @@ -200,6 +194,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
200 changes: 200 additions & 0 deletions src/Cli/dotnet/commands/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// 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;
using Microsoft.DotNet.Tools.Run;

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, ILogger consoleLogger)
{
var binaryLogger = GetBinaryLogger(binaryLoggerArgs);
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);
}

// Set up 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 = binaryLoggers.Length != 0,
};
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
// and https://github.com/dotnet/msbuild/issues/11519.
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)
{
// Like in MSBuild, only the last binary logger is used.
for (int i = args.Length - 1; i >= 0; i--)
{
var arg = args[i];
if (RunCommand.IsBinLogArgument(arg))
{
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)
{
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>
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

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

<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>

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

<!--
Override targets which don't work with project files that are not present on disk.
See https://github.com/NuGet/Home/issues/14148.
-->

<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.AddItem(itemType: "Compile", include: EntryPointFileFullPath);
projectRoot.FullPath = projectFileFullPath;
return projectRoot;
}
}
3 changes: 3 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,7 @@ 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>
</root>
13 changes: 9 additions & 4 deletions src/Cli/dotnet/commands/dotnet-run/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ public static RunCommand FromArgs(string[] args)
return FromParseResult(parseResult);
}

internal static bool IsBinLogArgument(string arg)
{
const StringComparison comp = StringComparison.OrdinalIgnoreCase;
return arg.StartsWith("/bl:", comp) || arg.Equals("/bl", comp)
|| arg.StartsWith("--binaryLogger:", comp) || arg.Equals("--binaryLogger", comp)
|| arg.StartsWith("-bl:", comp) || arg.Equals("-bl", comp);
}

public static RunCommand FromParseResult(ParseResult parseResult)
{
if (parseResult.UsingRunCommandShorthandProjectOption())
Expand All @@ -37,10 +45,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
var nonBinLogArgs = new List<string>();
foreach (var arg in applicationArguments)
{

if (arg.StartsWith("/bl:") || arg.Equals("/bl")
|| arg.StartsWith("--binaryLogger:") || arg.Equals("--binaryLogger")
|| arg.StartsWith("-bl:") || arg.Equals("-bl"))
if (IsBinLogArgument(arg))
{
binlogArgs.Add(arg);
}
Expand Down
Loading