diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 33ec7b5e3219..4235c5b0e431 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -6,6 +6,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; @@ -497,6 +498,7 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca } } + [DoesNotReturn] internal static void ThrowUnableToRunError(ProjectInstance project) { string targetFrameworks = project.GetPropertyValue("TargetFrameworks"); diff --git a/src/Cli/dotnet/Commands/Run/RunProperties.cs b/src/Cli/dotnet/Commands/Run/RunProperties.cs index 81e92cb88315..6972d140ac56 100644 --- a/src/Cli/dotnet/Commands/Run/RunProperties.cs +++ b/src/Cli/dotnet/Commands/Run/RunProperties.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Utils; @@ -19,9 +20,9 @@ internal RunProperties(string command, string? arguments, string? workingDirecto { } - internal static RunProperties FromProject(ProjectInstance project) + internal static bool TryFromProject(ProjectInstance project, [NotNullWhen(returnValue: true)] out RunProperties? result) { - var result = new RunProperties( + result = new RunProperties( Command: project.GetPropertyValue("RunCommand"), Arguments: project.GetPropertyValue("RunArguments"), WorkingDirectory: project.GetPropertyValue("RunWorkingDirectory"), @@ -30,6 +31,17 @@ internal static RunProperties FromProject(ProjectInstance project) TargetFrameworkVersion: project.GetPropertyValue("TargetFrameworkVersion")); if (string.IsNullOrEmpty(result.Command)) + { + result = null; + return false; + } + + return true; + } + + internal static RunProperties FromProject(ProjectInstance project) + { + if (!TryFromProject(project, out var result)) { RunCommand.ThrowUnableToRunError(project); } diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 6997b8052a0c..216f717c81df 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -353,7 +353,9 @@ public override int Execute() Debug.Assert(buildRequest.ProjectInstance != null); // Cache run info (to avoid re-evaluating the project instance). - cache.CurrentEntry.Run = RunProperties.FromProject(buildRequest.ProjectInstance); + cache.CurrentEntry.Run = RunProperties.TryFromProject(buildRequest.ProjectInstance, out var runProperties) + ? runProperties + : null; TryCacheCscArguments(cache, buildResult, buildRequest.ProjectInstance); @@ -897,7 +899,7 @@ private BuildLevel GetBuildLevel(out CacheInfo cache) { if (!NeedsToBuild(out cache)) { - Reporter.Verbose.WriteLine("No need to build, the output is up to date."); + Reporter.Verbose.WriteLine("No need to build, the output is up to date. Cache: " + ArtifactsPath); return BuildLevel.None; } diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 2c87e8cc7469..ecc6a6731057 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1483,6 +1483,230 @@ public void NoBuild_02() .And.HaveStdOut("Changed"); } + [Fact] + public void Build_Library() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Library + class C; + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Library_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Library + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + class C; + """); + + // https://github.com/dotnet/sdk/issues/51077: cannot set this via `#:property` directive. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "lib.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Module() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "module.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Module + #:property ProduceReferenceAssembly=false + class C; + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Module")); + } + + [Fact] + public void Build_WinExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "winexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=WinExe + Console.WriteLine("Hello WinExe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello WinExe"); + } + + [Fact] + public void Build_Exe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Exe + Console.WriteLine("Hello Exe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_Exe_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Exe + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + Console.WriteLine("Hello Exe"); + """); + + // https://github.com/dotnet/sdk/issues/51077: cannot set this via `#:property` directive. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "exe.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_AppContainerExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "appcontainerexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=AppContainerExe + Console.WriteLine("Hello AppContainerExe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "AppContainerExe")); + } + [Fact] public void Publish() {