diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index c8fddd91aead..8e70cb3b11db 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -125,6 +125,10 @@ Similarly, implicit build files like `Directory.Build.props` or `Directory.Packa > [!CAUTION] > Multi-file support is postponed for .NET 11. > In .NET 10, only the single file passed as the command-line argument to `dotnet run` is part of the compilation. +> Specifically, the virtual project has properties `EnableDefaultCompileItems=false` and `EnableDefaultEmbeddedResourceItems=false` +> (which can be customized via `#:property` directives), and a `Compile` item for the entry point file. +> During [conversion](#grow-up), any `Content`, `None`, `Compile`, and `EmbeddedResource` items that do not have metadata `ExcludeFromFileBasedAppConversion=true` +> and that are files inside the entry point file's directory tree are copied to the converted directory. ### Nested files diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 38c605d1e1ef..d4ae5207be00 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.Cli.Commands; @@ -32,6 +33,9 @@ public override int Execute() var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file); var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, errors: null); + // Find other items to copy over, e.g., default Content items like JSON files in Web apps. + var includeItems = FindIncludedItems().ToList(); + Directory.CreateDirectory(targetDirectory); var targetFile = Path.Join(targetDirectory, Path.GetFileName(file)); @@ -47,11 +51,71 @@ public override int Execute() File.Move(file, targetFile); } + // Create project file. string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj"); using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write); using var writer = new StreamWriter(stream, Encoding.UTF8); VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false); + // Copy over included items. + foreach (var item in includeItems) + { + string targetItemFullPath = Path.Combine(targetDirectory, item.RelativePath); + + // Ignore already-copied files. + if (File.Exists(targetItemFullPath)) + { + continue; + } + + string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!; + Directory.CreateDirectory(targetItemDirectory); + File.Copy(item.FullPath, targetItemFullPath); + } + return 0; + + IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems() + { + string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!); + var projectCollection = new ProjectCollection(); + var command = new VirtualProjectBuildingCommand( + entryPointFileFullPath: file, + msbuildArgs: MSBuildArgs.FromOtherArgs([])) + { + Directives = directives, + }; + var projectInstance = command.CreateProjectInstance(projectCollection); + + // Include only items we know are files. + string[] itemTypes = ["Content", "None", "Compile", "EmbeddedResource"]; + var items = itemTypes.SelectMany(t => projectInstance.GetItems(t)); + + foreach (var item in items) + { + // Escape hatch - exclude items that have metadata `ExcludeFromFileBasedAppConversion` set to `true`. + string include = item.GetMetadataValue("ExcludeFromFileBasedAppConversion"); + if (string.Equals(include, bool.TrueString, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Exclude items that are not contained within the entry point file directory. + string itemFullPath = Path.GetFullPath(path: item.GetMetadataValue("FullPath"), basePath: entryPointFileDirectory); + if (!itemFullPath.StartsWith(entryPointFileDirectory, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Exclude items that do not exist. + if (!File.Exists(itemFullPath)) + { + continue; + } + + string itemRelativePath = Path.GetRelativePath(relativeTo: entryPointFileDirectory, path: itemFullPath); + yield return (FullPath: itemFullPath, RelativePath: itemRelativePath); + } + } } } diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index a2ac5e1ba80b..aa6c6db3c950 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -94,8 +94,6 @@ public override RunApiOutput Execute() CustomArtifactsPath = ArtifactsPath, }; - buildCommand.PrepareProjectInstance(); - var runCommand = new RunCommand( noBuild: false, projectFileFullPath: null, diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 9d712a41a79c..efdf946c313e 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -140,7 +140,7 @@ public int Execute() if (EntryPointFileFullPath is not null) { Debug.Assert(!ReadCodeFromStdin); - projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance; + projectFactory = CreateVirtualCommand().CreateProjectInstance; } } @@ -587,9 +587,13 @@ public static RunCommand FromParseResult(ParseResult parseResult) } // If '-' is specified as the input file, read all text from stdin into a temporary file and use that as the entry point. - entryPointFilePath = Path.GetTempFileName(); + // We create a new directory for each file so other files are not included in the compilation. + // We fail if the file already exists to avoid reusing the same file for multiple stdin runs (in case the random name is duplicate). + string directory = VirtualProjectBuildingCommand.GetTempSubdirectory(Path.GetRandomFileName()); + VirtualProjectBuildingCommand.CreateTempSubdirectory(directory); + entryPointFilePath = Path.Join(directory, "app.cs"); using (var stdinStream = Console.OpenStandardInput()) - using (var fileStream = File.OpenWrite(entryPointFilePath)) + using (var fileStream = new FileStream(entryPointFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { stdinStream.CopyTo(fileStream); } diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index f0f0dfa1ded2..d042e5f638b6 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -111,8 +111,6 @@ Override targets which don't work with project files that are not present on dis """; - private ImmutableArray _directives; - public VirtualProjectBuildingCommand( string entryPointFileFullPath, MSBuildArgs msbuildArgs) @@ -136,6 +134,23 @@ public VirtualProjectBuildingCommand( /// public bool NoBuildMarkers { get; init; } + public ImmutableArray Directives + { + get + { + if (field.IsDefault) + { + var sourceFile = LoadSourceFile(EntryPointFileFullPath); + field = FindDirectives(sourceFile, reportAllErrors: false, errors: null); + Debug.Assert(!field.IsDefault); + } + + return field; + } + + init; + } + public override int Execute() { Debug.Assert(!(NoRestore && NoBuild)); @@ -157,8 +172,6 @@ public override int Execute() Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseUpToDate.Yellow()); } - PrepareProjectInstance(); - return 0; } @@ -188,8 +201,6 @@ public override int Execute() LogTaskInputs = binaryLoggers.Length != 0, }; - PrepareProjectInstance(); - // 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. @@ -445,17 +456,7 @@ private void MarkBuildStart() string directory = GetArtifactsPath(); - if (OperatingSystem.IsWindows()) - { - Directory.CreateDirectory(directory); - } - else - { - // Ensure only the current user has access to the directory to avoid leaking the program to other users. - // We don't mind that permissions might be different if the directory already exists, - // since it's under user's local directory and its path should be unique. - Directory.CreateDirectory(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); - } + CreateTempSubdirectory(directory); File.WriteAllText(Path.Join(directory, BuildStartCacheFileName), EntryPointFileFullPath); } @@ -472,19 +473,6 @@ private void MarkBuildSuccess(RunFileBuildCacheEntry cacheEntry) JsonSerializer.Serialize(stream, cacheEntry, RunFileJsonSerializerContext.Default.RunFileBuildCacheEntry); } - /// - /// Needs to be called before the first call to . - /// - public VirtualProjectBuildingCommand PrepareProjectInstance() - { - Debug.Assert(_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should not be called multiple times."); - - var sourceFile = LoadSourceFile(EntryPointFileFullPath); - _directives = FindDirectives(sourceFile, reportAllErrors: false, errors: null); - - return this; - } - public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection) { return CreateProjectInstance(projectCollection, addGlobalProperties: null); @@ -511,13 +499,11 @@ private ProjectInstance CreateProjectInstance( ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) { - Debug.Assert(!_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should have been called first."); - var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); var projectFileWriter = new StringWriter(); WriteProjectFile( projectFileWriter, - _directives, + Directives, isVirtualProject: true, targetFilePath: EntryPointFileFullPath, artifactsPath: GetArtifactsPath(), @@ -534,20 +520,46 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) private string GetArtifactsPath() => CustomArtifactsPath ?? GetArtifactsPath(EntryPointFileFullPath); - // internal for testing - internal static string GetArtifactsPath(string entryPointFileFullPath) + public static string GetArtifactsPath(string entryPointFileFullPath) + { + // 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 GetTempSubdirectory(directoryName); + } + + /// + /// Obtains a temporary subdirectory for file-based apps. + /// + public static string GetTempSubdirectory(string name) { // 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); - // 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 Path.Join(directory, "dotnet", "runfile", name); + } - return Path.Join(directory, "dotnet", "runfile", directoryName); + /// + /// Creates a temporary subdirectory for file-based apps. + /// Use to obtain the path. + /// + public static void CreateTempSubdirectory(string path) + { + if (OperatingSystem.IsWindows()) + { + Directory.CreateDirectory(path); + } + else + { + // Ensure only the current user has access to the directory to avoid leaking the program to other users. + // We don't mind that permissions might be different if the directory already exists, + // since it's under user's local directory and its path should be unique. + Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } } public static void WriteProjectFile( @@ -648,7 +660,7 @@ public static void WriteProjectFile( writer.WriteLine(""" - false + false """); } diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 4cf756e574ce..229f81f857e0 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -262,6 +262,279 @@ public void NestedDirectory() .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); } + /// + /// Default items like None or Content are copied over. + /// + [Fact] + public void DefaultItems() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine(); + """); + File.WriteAllText(Path.Join(testInstance.Path, "my.json"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), ""); + Directory.CreateDirectory(Path.Join(testInstance.Path, "subdir")); + File.WriteAllText(Path.Join(testInstance.Path, "subdir", "second.json"), ""); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json", "subdir"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Resources.resx", "Program.cs", "my.json", "subdir"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program", "subdir")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["second.json"]); + } + + [Fact] + public void DefaultItems_MoreIncluded() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:property EnableDefaultCompileItems=true + Console.WriteLine(); + """); + File.WriteAllText(Path.Join(testInstance.Path, "my.json"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), ""); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs", "Resources.resx", "Util.cs", "my.json"]); + } + + [Fact] + public void DefaultItems_MoreExcluded() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:property EnableDefaultItems=false + Console.WriteLine(); + """); + File.WriteAllText(Path.Join(testInstance.Path, "my.json"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), ""); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); + } + + /// + /// ExcludeFromFileBasedAppConversion metadata can be used to exclude items from the conversion. + /// + [Fact] + public void DefaultItems_ExcludedViaMetadata() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine(); + """); + File.WriteAllText(Path.Join(testInstance.Path, "my.json"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "second.json"), ""); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ + + + + + + """); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Directory.Build.targets", "Program", "Resources.resx", "Util.cs", "my.json", "second.json"]); + + // `second.json` is excluded from the conversion. + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Directory.Build.targets", "Program.csproj", "Resources.resx", "Program.cs", "my.json"]); + } + + [Fact] + public void DefaultItems_ImplicitBuildFileInDirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine(Util.GetText()); + """); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + class Util { public static string GetText() => "Hi from Util"; } + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + // The app works before conversion. + string expectedOutput = "Hi from Util"; + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert. + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Directory.Build.props", "Program", "Util.cs"]); + + // Directory.Build.props is included as it's a None item. + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Directory.Build.props", "Program.csproj", "Program.cs", "Util.cs"]); + + // The app works after conversion. + new DotnetCommand(Log, "run", "Program/Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void DefaultItems_ImplicitBuildFileOutsideDirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var subdir = Path.Join(testInstance.Path, "subdir"); + Directory.CreateDirectory(subdir); + File.WriteAllText(Path.Join(subdir, "Program.cs"), """ + Console.WriteLine(Util.GetText()); + """); + File.WriteAllText(Path.Join(subdir, "Util.cs"), """ + class Util { public static string GetText() => "Hi from Util"; } + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + // The app works before conversion. + string expectedOutput = "Hi from Util"; + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert. + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass(); + + new DirectoryInfo(subdir) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program", "Util.cs"]); + + new DirectoryInfo(Path.Join(subdir, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs", "Util.cs"]); + + // The app works after conversion. + new DotnetCommand(Log, "run", "Program/Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void DefaultItems_ImplicitBuildFileAndUtilOutsideDirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var subdir = Path.Join(testInstance.Path, "subdir"); + Directory.CreateDirectory(subdir); + File.WriteAllText(Path.Join(subdir, "Program.cs"), """ + Console.WriteLine(Util.GetText()); + """); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + class Util { public static string GetText() => "Hi from Util"; } + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + // The app works before conversion. + string expectedOutput = "Hi from Util"; + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert. + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass(); + + new DirectoryInfo(subdir) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program"]); + + new DirectoryInfo(Path.Join(subdir, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); + + // The app works after conversion. + new DotnetCommand(Log, "run", "Program/Program.cs") + .WithWorkingDirectory(subdir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + /// /// When processing fails due to invalid directives, no conversion should be performed /// (e.g., the target directory should not be created). @@ -283,6 +556,28 @@ public void ProcessingFails() .EnumerateDirectories().Should().BeEmpty(); } + /// + /// Since we perform MSBuild evaluation during the conversion (to find included items to copy over), + /// the conversion can fail when the specified SDK does not exist. + /// + [Fact] + public void ProcessingFails_Evaluation() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var filePath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(filePath, "#:sdk Microsoft.ThisSdkDoesNotExist"); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // The SDK 'Microsoft.ThisSdkDoesNotExist' specified could not be found. + .And.HaveStdErrContaining("Microsoft.ThisSdkDoesNotExist"); + + new DirectoryInfo(Path.Join(testInstance.Path)) + .EnumerateDirectories().Should().BeEmpty(); + } + /// /// End-to-end test of directive processing. More cases are covered by faster unit tests below. /// @@ -291,7 +586,7 @@ public void ProcessingSucceeds() { var testInstance = _testAssetsManager.CreateTestDirectory(); File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #:sdk Aspire.Hosting.Sdk@9.1.0 + #:package Humanizer@2.14.1 Console.WriteLine(); """); @@ -313,7 +608,7 @@ public void ProcessingSucceeds() File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj")) .Should().Be($""" - + Exe @@ -323,6 +618,10 @@ public void ProcessingSucceeds() true + + + + """); diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 2155342cf6be..a7887db1c0fb 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -578,6 +578,20 @@ public void MultipleFiles_RunEntryPoint() .Execute() .Should().Fail() .And.HaveStdOutContaining("error CS0103"); // The name 'Util' does not exist in the current context + + // This can be overridden. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:property EnableDefaultCompileItems=true + {s_programDependingOnUtil} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + // warning CS2002: Source file 'Program.cs' specified multiple times + .And.HaveStdOutContaining("warning CS2002") + .And.HaveStdOutContaining("Hello, String from Util"); } /// @@ -928,13 +942,13 @@ public void BinaryLog_EvaluationData() } /// - /// Default projects do not include anything apart from the entry-point file. + /// Default projects include embedded resources by default. /// [Fact] public void EmbeddedResource() { var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + string code = """ using var stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Program.Resources.resources"); if (stream is null) @@ -945,7 +959,8 @@ public void EmbeddedResource() using var reader = new System.Resources.ResourceReader(stream); Console.WriteLine(reader.Cast().Single()); - """); + """; + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), code); File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), """ @@ -954,6 +969,20 @@ public void EmbeddedResource() """); + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + [MyString, TestValue] + """); + + // This behavior can be overridden. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:property EnableDefaultEmbeddedResourceItems=false + {code} + """); + new DotnetCommand(Log, "run", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() @@ -1164,6 +1193,37 @@ public void PublishWithCustomTarget() ]); } + [Fact] + public void Publish_WithJson() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, """ + #:sdk Microsoft.NET.Sdk.Web + Console.WriteLine(File.ReadAllText("config.json")); + """); + + File.WriteAllText(Path.Join(testInstance.Path, "config.json"), """ + { "MyKey": "MyValue" } + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var publishDir = Path.Join(testInstance.Path, "artifacts"); + if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true); + + new DotnetCommand(Log, "publish", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(publishDir).Sub("Program") + .Should().Exist() + .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly) // no deps.json file for AOT-published app + .And.HaveFile("config.json"); // the JSON is included as content and hence copied + } + [Fact] public void Publish_Options() { @@ -1802,7 +1862,7 @@ public void Api() - false + false @@ -1880,7 +1940,7 @@ public void Api_Diagnostic_01() - false + false @@ -1952,7 +2012,7 @@ public void Api_Diagnostic_02() - false + false