Skip to content

Commit f98b22c

Browse files
authored
Support build/restore of file-based programs (#48662)
1 parent 00e076d commit f98b22c

35 files changed

+411
-121
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,11 @@ but that would require changes to the native dotnet host.
257257

258258
## Other commands
259259

260-
We can consider supporting other commands like `dotnet build`, `dotnet pack`, `dotnet watch`.
260+
Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs.
261+
We can consider supporting other commands like `dotnet pack`, `dotnet watch`,
262+
however the primary scenario is `dotnet run` and we might never support additional commands.
261263

262-
These commands need to have a way to receive the target path similarly to `dotnet run`,
264+
All commands supporting file-based programs should have a way to receive the target path similarly to `dotnet run`,
263265
e.g., via options like `--directory`/`--entry` as described [above](#integration-into-the-existing-dotnet-run-command),
264266
or as the first argument if it makes sense for them.
265267

src/Cli/dotnet/CliStrings.resx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,12 @@ setx PATH "%PATH%;{0}"
458458
<data name="SolutionOrProjectArgumentName" xml:space="preserve">
459459
<value>PROJECT | SOLUTION</value>
460460
</data>
461+
<data name="SolutionOrProjectOrFileArgumentDescription" xml:space="preserve">
462+
<value>The project or solution or C# (file-based program) file to operate on. If a file is not specified, the command will search the current directory for a project or solution.</value>
463+
</data>
464+
<data name="SolutionOrProjectOrFileArgumentName" xml:space="preserve">
465+
<value>PROJECT | SOLUTION | FILE</value>
466+
</data>
461467
<data name="CommandInteractiveOptionDescription" xml:space="preserve">
462468
<value>Allows the command to stop and wait for user input or action (for example to complete authentication).</value>
463469
</data>
@@ -797,4 +803,4 @@ For a list of locations searched, specify the "-d" option before the tool name.<
797803
<value>Cannot specify --version when the package argument already contains a version.</value>
798804
<comment>{Locked="--version"}</comment>
799805
</data>
800-
</root>
806+
</root>

src/Cli/dotnet/CommandBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ protected CommandBase(ParseResult parseResult)
1616
ShowHelpOrErrorIfAppropriate(parseResult);
1717
}
1818

19+
protected CommandBase() { }
20+
1921
protected virtual void ShowHelpOrErrorIfAppropriate(ParseResult parseResult)
2022
{
2123
parseResult.ShowHelpOrErrorIfAppropriate();

src/Cli/dotnet/Commands/Build/BuildCommand.cs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,73 @@
33

44
using System.CommandLine;
55
using Microsoft.DotNet.Cli.Commands.Restore;
6+
using Microsoft.DotNet.Cli.Commands.Run;
67
using Microsoft.DotNet.Cli.Extensions;
78

89
namespace Microsoft.DotNet.Cli.Commands.Build;
910

10-
public class BuildCommand(
11-
IEnumerable<string> msbuildArgs,
12-
bool noRestore,
13-
string msbuildPath = null) : RestoringCommand(msbuildArgs, noRestore, msbuildPath)
11+
public static class BuildCommand
1412
{
15-
public static BuildCommand FromArgs(string[] args, string msbuildPath = null)
13+
public static CommandBase FromArgs(string[] args, string msbuildPath = null)
1614
{
1715
var parser = Parser.Instance;
1816
var parseResult = parser.ParseFrom("dotnet build", args);
1917
return FromParseResult(parseResult, msbuildPath);
2018
}
2119

22-
public static BuildCommand FromParseResult(ParseResult parseResult, string msbuildPath = null)
20+
public static CommandBase FromParseResult(ParseResult parseResult, string msbuildPath = null)
2321
{
2422
PerformanceLogEventSource.Log.CreateBuildCommandStart();
2523

26-
var msbuildArgs = new List<string>();
27-
2824
parseResult.ShowHelpOrErrorIfAppropriate();
2925

3026
CommonOptions.ValidateSelfContainedOptions(
3127
parseResult.GetResult(BuildCommandParser.SelfContainedOption) is not null,
3228
parseResult.GetResult(BuildCommandParser.NoSelfContainedOption) is not null);
3329

34-
msbuildArgs.Add($"-consoleloggerparameters:Summary");
30+
string[] fileArgument = parseResult.GetValue(BuildCommandParser.SlnOrProjectOrFileArgument) ?? [];
31+
32+
string[] forwardedOptions = parseResult.OptionValuesToBeForwarded(BuildCommandParser.GetCommand()).ToArray();
33+
34+
bool noRestore = parseResult.GetResult(BuildCommandParser.NoRestoreOption) is not null;
35+
36+
bool noIncremental = parseResult.GetResult(BuildCommandParser.NoIncrementalOption) is not null;
3537

36-
if (parseResult.GetResult(BuildCommandParser.NoIncrementalOption) is not null)
38+
CommandBase command;
39+
40+
if (fileArgument is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg))
3741
{
38-
msbuildArgs.Add("-target:Rebuild");
42+
command = new VirtualProjectBuildingCommand(
43+
entryPointFileFullPath: Path.GetFullPath(arg),
44+
msbuildArgs: forwardedOptions,
45+
verbosity: parseResult.GetValue(CommonOptions.VerbosityOption),
46+
interactive: parseResult.GetValue(CommonOptions.InteractiveMsBuildForwardOption))
47+
{
48+
NoRestore = noRestore,
49+
NoCache = true,
50+
NoIncremental = noIncremental,
51+
};
3952
}
40-
var arguments = parseResult.GetValue(BuildCommandParser.SlnOrProjectArgument) ?? [];
53+
else
54+
{
55+
var msbuildArgs = new List<string>();
4156

42-
msbuildArgs.AddRange(parseResult.OptionValuesToBeForwarded(BuildCommandParser.GetCommand()));
57+
msbuildArgs.Add($"-consoleloggerparameters:Summary");
4358

44-
msbuildArgs.AddRange(arguments);
59+
if (noIncremental)
60+
{
61+
msbuildArgs.Add("-target:Rebuild");
62+
}
4563

46-
bool noRestore = parseResult.GetResult(BuildCommandParser.NoRestoreOption) is not null;
64+
msbuildArgs.AddRange(forwardedOptions);
4765

48-
BuildCommand command = new(
49-
msbuildArgs,
50-
noRestore,
51-
msbuildPath);
66+
msbuildArgs.AddRange(fileArgument);
67+
68+
command = new RestoringCommand(
69+
msbuildArgs: msbuildArgs,
70+
noRestore: noRestore,
71+
msbuildPath: msbuildPath);
72+
}
5273

5374
PerformanceLogEventSource.Log.CreateBuildCommandStop();
5475

src/Cli/dotnet/Commands/Build/BuildCommandParser.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ internal static class BuildCommandParser
1111
{
1212
public static readonly string DocsLink = "https://aka.ms/dotnet-build";
1313

14-
public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
14+
public static readonly Argument<string[]> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
1515
{
16-
Description = CliStrings.SolutionOrProjectArgumentDescription,
16+
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
1717
Arity = ArgumentArity.ZeroOrMore
1818
};
1919

@@ -64,7 +64,7 @@ private static Command ConstructCommand()
6464
{
6565
DocumentedCommand command = new("build", DocsLink, CliCommandStrings.BuildAppFullName);
6666

67-
command.Arguments.Add(SlnOrProjectArgument);
67+
command.Arguments.Add(SlnOrProjectOrFileArgument);
6868
RestoreCommandParser.AddImplicitRestoreOptions(command, includeRuntimeOption: false, includeNoDependenciesOption: false);
6969
command.Options.Add(FrameworkOption);
7070
command.Options.Add(ConfigurationOption);

src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace Microsoft.DotNet.Cli.Commands.MSBuild;
1010

11-
public class MSBuildForwardingApp
11+
public class MSBuildForwardingApp : CommandBase
1212
{
1313
internal const string TelemetrySessionIdEnvironmentVariableName = "DOTNET_CLI_TELEMETRY_SESSIONID";
1414

@@ -74,7 +74,7 @@ private void InitializeRequiredEnvironmentVariables()
7474

7575
internal string[] GetArgumentTokensToMSBuild() => _forwardingAppWithoutLogging.GetAllArguments();
7676

77-
public virtual int Execute()
77+
public override int Execute()
7878
{
7979
// Ignore Ctrl-C for the remainder of the command's execution
8080
// Forwarding commands will just spawn the child process and exit

src/Cli/dotnet/Commands/Restore/RestoreCommand.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,52 @@
33

44
using System.CommandLine;
55
using Microsoft.DotNet.Cli.Commands.MSBuild;
6+
using Microsoft.DotNet.Cli.Commands.Run;
67
using Microsoft.DotNet.Cli.Extensions;
78
using Microsoft.DotNet.Cli.Utils;
89

910
namespace Microsoft.DotNet.Cli.Commands.Restore;
1011

11-
public class RestoreCommand : MSBuildForwardingApp
12+
public static class RestoreCommand
1213
{
13-
public RestoreCommand(IEnumerable<string> msbuildArgs, string msbuildPath = null)
14-
: base(msbuildArgs, msbuildPath)
15-
{
16-
NuGetSignatureVerificationEnabler.ConditionallyEnable(this);
17-
}
18-
19-
public static RestoreCommand FromArgs(string[] args, string msbuildPath = null)
14+
public static CommandBase FromArgs(string[] args, string msbuildPath = null)
2015
{
2116
var parser = Parser.Instance;
2217
var result = parser.ParseFrom("dotnet restore", args);
2318
return FromParseResult(result, msbuildPath);
2419
}
2520

26-
public static RestoreCommand FromParseResult(ParseResult result, string msbuildPath = null)
21+
public static CommandBase FromParseResult(ParseResult result, string msbuildPath = null)
2722
{
2823
result.HandleDebugSwitch();
2924

3025
result.ShowHelpOrErrorIfAppropriate();
3126

32-
List<string> msbuildArgs = ["-target:Restore"];
27+
string[] fileArgument = result.GetValue(RestoreCommandParser.SlnOrProjectOrFileArgument) ?? [];
3328

34-
msbuildArgs.AddRange(result.OptionValuesToBeForwarded(RestoreCommandParser.GetCommand()));
29+
string[] forwardedOptions = result.OptionValuesToBeForwarded(RestoreCommandParser.GetCommand()).ToArray();
3530

36-
msbuildArgs.AddRange(result.GetValue(RestoreCommandParser.SlnOrProjectArgument) ?? []);
31+
if (fileArgument is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg))
32+
{
33+
return new VirtualProjectBuildingCommand(
34+
entryPointFileFullPath: Path.GetFullPath(arg),
35+
msbuildArgs: forwardedOptions,
36+
verbosity: result.GetValue(CommonOptions.VerbosityOption),
37+
interactive: result.GetValue(CommonOptions.InteractiveMsBuildForwardOption))
38+
{
39+
NoCache = true,
40+
NoBuild = true,
41+
};
42+
}
3743

38-
return new RestoreCommand(msbuildArgs, msbuildPath);
44+
return CreateForwarding(["-target:Restore", .. forwardedOptions, .. fileArgument], msbuildPath);
45+
}
46+
47+
public static MSBuildForwardingApp CreateForwarding(IEnumerable<string> msbuildArgs, string? msbuildPath = null)
48+
{
49+
var forwardingApp = new MSBuildForwardingApp(msbuildArgs, msbuildPath);
50+
NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp);
51+
return forwardingApp;
3952
}
4053

4154
public static int Run(string[] args)

src/Cli/dotnet/Commands/Restore/RestoreCommandParser.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ internal static class RestoreCommandParser
1010
{
1111
public static readonly string DocsLink = "https://aka.ms/dotnet-restore";
1212

13-
public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
13+
public static readonly Argument<string[]> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
1414
{
15-
Description = CliStrings.SolutionOrProjectArgumentDescription,
15+
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
1616
Arity = ArgumentArity.ZeroOrMore
1717
};
1818

@@ -62,7 +62,7 @@ private static Command ConstructCommand()
6262
{
6363
var command = new DocumentedCommand("restore", DocsLink, CliCommandStrings.RestoreAppFullName);
6464

65-
command.Arguments.Add(SlnOrProjectArgument);
65+
command.Arguments.Add(SlnOrProjectOrFileArgument);
6666
command.Options.Add(CommonOptions.DisableBuildServersOption);
6767

6868
foreach (var option in FullRestoreOptions())

src/Cli/dotnet/Commands/Restore/RestoringCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Cli.Commands.Restore;
99

1010
public class RestoringCommand : MSBuildForwardingApp
1111
{
12-
public RestoreCommand SeparateRestoreCommand { get; }
12+
public MSBuildForwardingApp SeparateRestoreCommand { get; }
1313

1414
private readonly bool AdvertiseWorkloadUpdates;
1515

@@ -49,7 +49,7 @@ private static IEnumerable<string> GetCommandArguments(
4949
return Prepend("-restore", arguments);
5050
}
5151

52-
private static RestoreCommand GetSeparateRestoreCommand(
52+
private static MSBuildForwardingApp GetSeparateRestoreCommand(
5353
IEnumerable<string> arguments,
5454
bool noRestore,
5555
string msbuildPath)
@@ -63,7 +63,7 @@ private static RestoreCommand GetSeparateRestoreCommand(
6363
(var newArgumentsToAdd, var existingArgumentsToForward) = ProcessForwardedArgumentsForSeparateRestore(arguments);
6464
restoreArguments = [.. restoreArguments, .. newArgumentsToAdd, .. existingArgumentsToForward];
6565

66-
return new RestoreCommand(restoreArguments, msbuildPath);
66+
return RestoreCommand.CreateForwarding(restoreArguments, msbuildPath);
6767
}
6868

6969
private static IEnumerable<string> Prepend(string argument, IEnumerable<string> arguments)

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,7 @@ public int Execute()
114114
{
115115
if (EntryPointFileFullPath is not null)
116116
{
117-
projectFactory = new VirtualProjectBuildingCommand
118-
{
119-
EntryPointFileFullPath = EntryPointFileFullPath,
120-
}.PrepareProjectInstance().CreateProjectInstance;
117+
projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance;
121118
}
122119

123120
if (NoCache)
@@ -256,19 +253,9 @@ private void EnsureProjectIsBuilt(out Func<ProjectCollection, ProjectInstance>?
256253
int buildResult;
257254
if (EntryPointFileFullPath is not null)
258255
{
259-
var command = new VirtualProjectBuildingCommand
260-
{
261-
EntryPointFileFullPath = EntryPointFileFullPath,
262-
};
263-
264-
CommonRunHelpers.AddUserPassedProperties(command.GlobalProperties, RestoreArgs);
265-
256+
var command = CreateVirtualCommand();
266257
projectFactory = command.CreateProjectInstance;
267-
buildResult = command.Execute(
268-
binaryLoggerArgs: RestoreArgs,
269-
consoleLogger: MakeTerminalLogger(Verbosity ?? GetDefaultVerbosity()),
270-
noRestore: NoRestore,
271-
noCache: NoCache);
258+
buildResult = command.Execute();
272259
}
273260
else
274261
{
@@ -289,25 +276,40 @@ private void EnsureProjectIsBuilt(out Func<ProjectCollection, ProjectInstance>?
289276
}
290277
}
291278

279+
private VirtualProjectBuildingCommand CreateVirtualCommand()
280+
{
281+
Debug.Assert(EntryPointFileFullPath != null);
282+
283+
return new(
284+
entryPointFileFullPath: EntryPointFileFullPath,
285+
msbuildArgs: RestoreArgs,
286+
verbosity: Verbosity,
287+
interactive: Interactive)
288+
{
289+
NoRestore = NoRestore,
290+
NoCache = NoCache,
291+
};
292+
}
293+
292294
private string[] GetRestoreArguments(IEnumerable<string> cliRestoreArgs)
293295
{
294296
List<string> args = ["-nologo"];
295297

296298
if (Verbosity is null)
297299
{
298-
args.Add($"-verbosity:{GetDefaultVerbosity()}");
300+
args.Add($"-verbosity:{GetDefaultVerbosity(Interactive)}");
299301
}
300302

301303
args.AddRange(cliRestoreArgs);
302304

303305
return [.. args];
304306
}
305307

306-
private VerbosityOptions GetDefaultVerbosity()
308+
internal static VerbosityOptions GetDefaultVerbosity(bool interactive)
307309
{
308310
// --interactive need to output guide for auth. It cannot be
309311
// completely "quiet"
310-
return Interactive ? VerbosityOptions.minimal : VerbosityOptions.quiet;
312+
return interactive ? VerbosityOptions.minimal : VerbosityOptions.quiet;
311313
}
312314

313315
private ICommand GetTargetCommand(Func<ProjectCollection, ProjectInstance>? projectFactory)
@@ -394,8 +396,7 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, string[] restoreAr
394396
}
395397
}
396398

397-
398-
static ILogger MakeTerminalLogger(VerbosityOptions? verbosity)
399+
internal static ILogger MakeTerminalLogger(VerbosityOptions? verbosity)
399400
{
400401
var msbuildVerbosity = ToLoggerVerbosity(verbosity);
401402

0 commit comments

Comments
 (0)