diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md
index a14132092ee1..77ea47410b0f 100644
--- a/documentation/general/dotnet-run-file.md
+++ b/documentation/general/dotnet-run-file.md
@@ -248,6 +248,16 @@ The directives are processed as follows:
(because `ProjectReference` items don't support directory paths).
An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do.
+Directive values support MSBuild variables (like `$(..)`) normally as they are translated literally and left to MSBuild engine to process.
+However, in `#:project` directives, variables might not be preserved during [grow up](#grow-up),
+because there is additional processing of those directives that makes it technically challenging to preserve variables in all cases
+(project directive values need to be resolved to be relative to the target directory
+and also to point to a project file rather than a directory).
+Note that it is not expected that variables inside the path change their meaning during the conversion,
+so for example `#:project ../$(LibName)` is translated to `` (i.e., the variable is preserved).
+However, variables at the start can change, so for example `#:project $(ProjectDir)../Lib` is translated to `` (i.e., the variable is expanded).
+In other directives, all variables are preserved during conversion.
+
Because these directives are limited by the C# language to only appear before the first "C# token" and any `#if`,
dotnet CLI can look for them via a regex or Roslyn lexer without any knowledge of defined conditional symbols
and can do that efficiently by stopping the search when it sees the first "C# token".
diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
index 22fd4000c0ff..1157bc586313 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
@@ -6,12 +6,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Xml;
+using Microsoft.Build.Execution;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -225,6 +227,30 @@ static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int in
}
}
}
+
+ ///
+ /// If there are any #:project , expands $() in them and ensures they point to project files (not directories).
+ ///
+ public static ImmutableArray EvaluateDirectives(
+ ProjectInstance? project,
+ ImmutableArray directives,
+ SourceFile sourceFile,
+ DiagnosticBag diagnostics)
+ {
+ if (directives.OfType().Any())
+ {
+ return directives
+ .Select(d => d is CSharpDirective.Project p
+ ? (project is null
+ ? p
+ : p.WithName(project.ExpandString(p.Name), CSharpDirective.Project.NameKind.Expanded))
+ .EnsureProjectFilePath(sourceFile, diagnostics)
+ : d)
+ .ToImmutableArray();
+ }
+
+ return directives;
+ }
}
internal readonly record struct SourceFile(string Path, SourceText Text)
@@ -457,8 +483,32 @@ public sealed class Package(in ParseInfo info) : Named(info)
///
/// #:project directive.
///
- public sealed class Project(in ParseInfo info) : Named(info)
+ public sealed class Project : Named
{
+ [SetsRequiredMembers]
+ public Project(in ParseInfo info, string name) : base(info)
+ {
+ Name = name;
+ OriginalName = name;
+ }
+
+ ///
+ /// Preserved across calls, i.e.,
+ /// this is the original directive text as entered by the user.
+ ///
+ public string OriginalName { get; init; }
+
+ ///
+ /// This is the with MSBuild $(..) vars expanded (via ).
+ ///
+ public string? ExpandedName { get; init; }
+
+ ///
+ /// This is the resolved via
+ /// (i.e., this is a file path if the original text pointed to a directory).
+ ///
+ public string? ProjectFilePath { get; init; }
+
public static new Project? Parse(in ParseContext context)
{
var directiveText = context.DirectiveText;
@@ -468,19 +518,57 @@ public sealed class Project(in ParseInfo info) : Named(info)
return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind));
}
+ return new Project(context.Info, directiveText);
+ }
+
+ public enum NameKind
+ {
+ ///
+ /// Change and .
+ ///
+ Expanded = 1,
+
+ ///
+ /// Change and .
+ ///
+ ProjectFilePath = 2,
+
+ ///
+ /// Change only .
+ ///
+ Final = 3,
+ }
+
+ public Project WithName(string name, NameKind kind)
+ {
+ return new Project(Info, name)
+ {
+ OriginalName = OriginalName,
+ ExpandedName = kind == NameKind.Expanded ? name : ExpandedName,
+ ProjectFilePath = kind == NameKind.ProjectFilePath ? name : ProjectFilePath,
+ };
+ }
+
+ ///
+ /// If the directive points to a directory, returns a new directive pointing to the corresponding project file.
+ ///
+ public Project EnsureProjectFilePath(SourceFile sourceFile, DiagnosticBag diagnostics)
+ {
+ var resolvedName = Name;
+
try
{
// If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'.
- // Also normalize blackslashes to forward slashes to ensure the directive works on all platforms.
- // https://github.com/dotnet/sdk/issues/51487: Behavior should not depend on process current directory
- var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? ".";
- var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/'));
+ // Also normalize backslashes to forward slashes to ensure the directive works on all platforms.
+ var sourceDirectory = Path.GetDirectoryName(sourceFile.Path)
+ ?? throw new InvalidOperationException($"Source file path '{sourceFile.Path}' does not have a containing directory.");
+ var resolvedProjectPath = Path.Combine(sourceDirectory, resolvedName.Replace('\\', '/'));
if (Directory.Exists(resolvedProjectPath))
{
var fullFilePath = GetProjectFileFromDirectory(resolvedProjectPath).FullName;
// Keep a relative path only if the original directive was a relative path.
- directiveText = ExternalHelpers.IsPathFullyQualified(directiveText)
+ resolvedName = ExternalHelpers.IsPathFullyQualified(resolvedName)
? fullFilePath
: ExternalHelpers.GetRelativePath(relativeTo: sourceDirectory, fullFilePath);
}
@@ -491,22 +579,14 @@ public sealed class Project(in ParseInfo info) : Named(info)
}
catch (GracefulException e)
{
- context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, e.Message), e);
+ diagnostics.AddError(sourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, e.Message), e);
}
- return new Project(context.Info)
- {
- Name = directiveText,
- };
- }
-
- public Project WithName(string name)
- {
- return new Project(Info) { Name = name };
+ return WithName(resolvedName, NameKind.ProjectFilePath);
}
// https://github.com/dotnet/sdk/issues/51487: Delete copies of methods from MsbuildProject and MSBuildUtilities from the source package, sharing the original method(s) under src/Cli instead.
- public static FileInfo GetProjectFileFromDirectory(string projectDirectory)
+ private static FileInfo GetProjectFileFromDirectory(string projectDirectory)
{
DirectoryInfo dir;
try
diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
index 84565c88284c..fad31237acf9 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
@@ -30,8 +30,19 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.Span.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.get -> Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project
-Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Project(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
-Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.WithName(string! name) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.EnsureProjectFilePath(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.get -> string?
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.init -> void
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.ProjectFilePath = 2 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.OriginalName.get -> string!
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.OriginalName.init -> void
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Project(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info, string! name) -> void
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ProjectFilePath.get -> string?
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ProjectFilePath.init -> void
+Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.WithName(string! name, Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind kind) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Property(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.get -> string!
@@ -102,7 +113,6 @@ override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Shebang.ToString() -
override Microsoft.DotNet.FileBasedPrograms.SourceFile.GetHashCode() -> int
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named?
-static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.GetProjectFileFromDirectory(string! projectDirectory) -> System.IO.FileInfo!
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk?
@@ -113,6 +123,7 @@ static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.CombineHashCodes(int v
static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.GetRelativePath(string! relativeTo, string! path) -> string!
static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.IsPathFullyQualified(string! path) -> bool
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.CreateTokenizer(Microsoft.CodeAnalysis.Text.SourceText! text) -> Microsoft.CodeAnalysis.CSharp.SyntaxTokenParser!
+static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.EvaluateDirectives(Microsoft.Build.Execution.ProjectInstance? project, System.Collections.Immutable.ImmutableArray directives, Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> System.Collections.Immutable.ImmutableArray
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, bool reportAllErrors, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> System.Collections.Immutable.ImmutableArray
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindLeadingDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.CodeAnalysis.SyntaxTriviaList triviaList, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics, System.Collections.Immutable.ImmutableArray.Builder? builder) -> void
static Microsoft.DotNet.FileBasedPrograms.MSBuildUtilities.ConvertStringToBool(string? parameterValue, bool defaultValue = false) -> bool
diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj
index ead3a31bed97..c0578c734abe 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj
@@ -26,7 +26,8 @@
-
+
+
diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
index e41367a34fd0..6acf2b7cc63b 100644
--- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
+++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.CommandLine;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Commands.Run;
@@ -31,7 +32,8 @@ public override int Execute()
// Find directives (this can fail, so do this before creating the target directory).
var sourceFile = SourceFile.Load(file);
- var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !_force, DiagnosticBag.ThrowOnFirst());
+ var diagnostics = DiagnosticBag.ThrowOnFirst();
+ var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !_force, diagnostics);
// Create a project instance for evaluation.
var projectCollection = new ProjectCollection();
@@ -43,6 +45,11 @@ public override int Execute()
};
var projectInstance = command.CreateProjectInstance(projectCollection);
+ // Evaluate directives.
+ directives = FileLevelDirectiveHelpers.EvaluateDirectives(projectInstance, directives, sourceFile, diagnostics);
+ command.Directives = directives;
+ projectInstance = command.CreateProjectInstance(projectCollection);
+
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
var includeItems = FindIncludedItems().ToList();
@@ -167,21 +174,58 @@ void CopyFile(string source, string target)
ImmutableArray UpdateDirectives(ImmutableArray directives)
{
+ var sourceDirectory = Path.GetDirectoryName(file)!;
+
var result = ImmutableArray.CreateBuilder(directives.Length);
foreach (var directive in directives)
{
- // Fixup relative project reference paths (they need to be relative to the output directory instead of the source directory).
- if (directive is CSharpDirective.Project project &&
- !Path.IsPathFullyQualified(project.Name))
- {
- var modified = project.WithName(Path.GetRelativePath(relativeTo: targetDirectory, path: project.Name));
- result.Add(modified);
- }
- else
+ // Fixup relative project reference paths (they need to be relative to the output directory instead of the source directory,
+ // and preserve MSBuild interpolation variables like `$(..)`
+ // while also pointing to the project file rather than a directory).
+ if (directive is CSharpDirective.Project project)
{
- result.Add(directive);
+ Debug.Assert(project.ExpandedName != null && project.ProjectFilePath != null && project.ProjectFilePath == project.Name);
+
+ if (Path.IsPathFullyQualified(project.Name))
+ {
+ // If the path is absolute and has no `$(..)` vars, just keep it.
+ if (project.ExpandedName == project.OriginalName)
+ {
+ result.Add(project);
+ continue;
+ }
+
+ // If the path is absolute and it *starts* with some `$(..)` vars,
+ // turn it into a relative path (it might be in the form `$(ProjectDir)/../Lib`
+ // and we don't want that to be turned into an absolute path in the converted project).
+ //
+ // If the path is absolute but the `$(..)` vars are *inside* of it (like `C:\$(..)\Lib`),
+ // instead of at the start, we can keep those vars, i.e., skip this `if` block.
+ //
+ // The `OriginalName` is absolute if there are no `$(..)` vars at the start.
+ if (!Path.IsPathFullyQualified(project.OriginalName))
+ {
+ project = project.WithName(Path.GetRelativePath(relativeTo: targetDirectory, path: project.Name), CSharpDirective.Project.NameKind.Final);
+ result.Add(project);
+ continue;
+ }
+ }
+
+ // If the original path is to a directory, just append the resolved file name
+ // but preserve the variables from the original, e.g., `../$(..)/Directory/Project.csproj`.
+ if (Directory.Exists(Path.Combine(sourceDirectory, project.ExpandedName)))
+ {
+ var projectFileName = Path.GetFileName(project.Name);
+ project = project.WithName(Path.Join(project.OriginalName, projectFileName), CSharpDirective.Project.NameKind.Final);
+ }
+
+ project = project.WithName(Path.GetRelativePath(relativeTo: targetDirectory, path: Path.Combine(sourceDirectory, project.Name)), CSharpDirective.Project.NameKind.Final);
+ result.Add(project);
+ continue;
}
+
+ result.Add(directive);
}
return result.DrainToImmutable();
diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
index c6ab42bbc498..ddd68ad109f5 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
@@ -9,7 +8,6 @@
using System.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
-using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Definition;
@@ -19,8 +17,6 @@
using Microsoft.Build.Logging;
using Microsoft.Build.Logging.SimpleErrorLogger;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts;
using Microsoft.DotNet.Cli.Commands.Restore;
@@ -165,14 +161,26 @@ public VirtualProjectBuildingCommand(
///
public bool NoWriteBuildMarkers { get; init; }
+ private SourceFile EntryPointSourceFile
+ {
+ get
+ {
+ if (field == default)
+ {
+ field = SourceFile.Load(EntryPointFileFullPath);
+ }
+
+ return field;
+ }
+ }
+
public ImmutableArray Directives
{
get
{
if (field.IsDefault)
{
- var sourceFile = SourceFile.Load(EntryPointFileFullPath);
- field = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.ThrowOnFirst());
+ field = FileLevelDirectiveHelpers.FindDirectives(EntryPointSourceFile, reportAllErrors: false, DiagnosticBag.ThrowOnFirst());
Debug.Assert(!field.IsDefault);
}
@@ -1050,6 +1058,23 @@ public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection
private ProjectInstance CreateProjectInstance(
ProjectCollection projectCollection,
Action>? addGlobalProperties)
+ {
+ var project = CreateProjectInstance(projectCollection, Directives, addGlobalProperties);
+
+ var directives = FileLevelDirectiveHelpers.EvaluateDirectives(project, Directives, EntryPointSourceFile, DiagnosticBag.ThrowOnFirst());
+ if (directives != Directives)
+ {
+ Directives = directives;
+ project = CreateProjectInstance(projectCollection, directives, addGlobalProperties);
+ }
+
+ return project;
+ }
+
+ private ProjectInstance CreateProjectInstance(
+ ProjectCollection projectCollection,
+ ImmutableArray directives,
+ Action>? addGlobalProperties)
{
var projectRoot = CreateProjectRootElement(projectCollection);
@@ -1072,7 +1097,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
var projectFileWriter = new StringWriter();
WriteProjectFile(
projectFileWriter,
- Directives,
+ directives,
isVirtualProject: true,
targetFilePath: EntryPointFileFullPath,
artifactsPath: ArtifactsPath,
diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
index 8532c6231b9f..14beaed209a7 100644
--- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
+++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
@@ -75,12 +75,17 @@ public void SameAsTemplate()
}
[Theory] // https://github.com/dotnet/sdk/issues/50832
- [InlineData("File", "Lib", "../Lib", "Project", "../Lib/lib.csproj")]
- [InlineData(".", "Lib", "./Lib", "Project", "../Lib/lib.csproj")]
- [InlineData(".", "Lib", "Lib/../Lib", "Project", "../Lib/lib.csproj")]
- [InlineData("File", "Lib", "../Lib", "File/Project", "../../Lib/lib.csproj")]
- [InlineData("File", "Lib", "..\\Lib", "File/Project", "../../Lib/lib.csproj")]
- public void ProjectReference_RelativePaths(string fileDir, string libraryDir, string reference, string outputDir, string convertedReference)
+ [InlineData("File", "File", "Lib", "../Lib", "Project", "..{/}Lib{/}lib.csproj")]
+ [InlineData(".", ".", "Lib", "./Lib", "Project", "..{/}Lib{/}lib.csproj")]
+ [InlineData(".", ".", "Lib", "Lib/../Lib", "Project", "..{/}Lib{/}lib.csproj")]
+ [InlineData("File", "File", "Lib", "../Lib", "File/Project", "..{/}..{/}Lib{/}lib.csproj")]
+ [InlineData(".", "File", "Lib", "../Lib", "File/Project", "..{/}..{/}Lib{/}lib.csproj")]
+ [InlineData("File", "File", "Lib", @"..\Lib", "File/Project", @"..{/}..\Lib{/}lib.csproj")]
+ [InlineData("File", "File", "Lib", "../$(LibProjectName)", "File/Project", "..{/}..{/}$(LibProjectName){/}lib.csproj")]
+ [InlineData(".", "File", "Lib", "../$(LibProjectName)", "File/Project", "..{/}..{/}$(LibProjectName){/}lib.csproj")]
+ [InlineData("File", "File", "Lib", @"..\$(LibProjectName)", "File/Project", @"..{/}..\$(LibProjectName){/}lib.csproj")]
+ [InlineData("File", "File", "Lib", "$(MSBuildProjectDirectory)/../$(LibProjectName)", "File/Project", "..{/}..{/}Lib{/}lib.csproj")]
+ public void ProjectReference_RelativePaths(string workingDir, string fileDir, string libraryDir, string reference, string outputDir, string convertedReference)
{
var testInstance = _testAssetsManager.CreateTestDirectory();
@@ -105,22 +110,26 @@ public static void M()
var fileDirFullPath = Path.Join(testInstance.Path, fileDir);
Directory.CreateDirectory(fileDirFullPath);
- File.WriteAllText(Path.Join(fileDirFullPath, "app.cs"), $"""
+ var fileFullPath = Path.Join(fileDirFullPath, "app.cs");
+ File.WriteAllText(fileFullPath, $"""
#:project {reference}
+ #:property LibProjectName=Lib
C.M();
""");
var expectedOutput = "Hello from library";
+ var workingDirFullPath = Path.Join(testInstance.Path, workingDir);
+ var fileRelativePath = Path.GetRelativePath(relativeTo: workingDirFullPath, path: fileFullPath);
- new DotnetCommand(Log, "run", "app.cs")
- .WithWorkingDirectory(fileDirFullPath)
+ new DotnetCommand(Log, "run", fileRelativePath)
+ .WithWorkingDirectory(workingDirFullPath)
.Execute()
.Should().Pass()
.And.HaveStdOut(expectedOutput);
var outputDirFullPath = Path.Join(testInstance.Path, outputDir);
- new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
- .WithWorkingDirectory(fileDirFullPath)
+ new DotnetCommand(Log, "project", "convert", fileRelativePath, "-o", outputDirFullPath)
+ .WithWorkingDirectory(workingDirFullPath)
.Execute()
.Should().Pass();
@@ -132,7 +141,7 @@ public static void M()
File.ReadAllText(Path.Join(outputDirFullPath, "app.csproj"))
.Should().Contain($"""
-
+
""");
}
@@ -193,6 +202,64 @@ public static void M()
""");
}
+ [Fact]
+ public void ProjectReference_FullPath_WithVars()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var libraryDirFullPath = Path.Join(testInstance.Path, "Lib");
+ Directory.CreateDirectory(libraryDirFullPath);
+ File.WriteAllText(Path.Join(libraryDirFullPath, "lib.cs"), """
+ public static class C
+ {
+ public static void M()
+ {
+ System.Console.WriteLine("Hello from library");
+ }
+ }
+ """);
+ File.WriteAllText(Path.Join(libraryDirFullPath, "lib.csproj"), $"""
+
+
+ {ToolsetInfo.CurrentTargetFramework}
+
+
+ """);
+
+ var fileDirFullPath = Path.Join(testInstance.Path, "File");
+ Directory.CreateDirectory(fileDirFullPath);
+ File.WriteAllText(Path.Join(fileDirFullPath, "app.cs"), $"""
+ #:project {fileDirFullPath}/../$(LibProjectName)
+ #:property LibProjectName=Lib
+ C.M();
+ """);
+
+ var expectedOutput = "Hello from library";
+
+ new DotnetCommand(Log, "run", "app.cs")
+ .WithWorkingDirectory(fileDirFullPath)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "File/Project");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(fileDirFullPath)
+ .Execute()
+ .Should().Pass();
+
+ new DotnetCommand(Log, "run")
+ .WithWorkingDirectory(outputDirFullPath)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+
+ File.ReadAllText(Path.Join(outputDirFullPath, "app.csproj"))
+ .Should().Contain($"""
+
+ """);
+ }
+
[Fact]
public void DirectoryAlreadyExists()
{
@@ -1631,6 +1698,7 @@ private static void Convert(string inputCSharp, out string actualProject, out st
actualDiagnostics = null;
var diagnosticBag = collectDiagnostics ? DiagnosticBag.Collect(out actualDiagnostics) : DiagnosticBag.ThrowOnFirst();
var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag);
+ directives = FileLevelDirectiveHelpers.EvaluateDirectives(project: null, directives, sourceFile, diagnosticBag);
var projectWriter = new StringWriter();
VirtualProjectBuildingCommand.WriteProjectFile(projectWriter, directives, isVirtualProject: false);
actualProject = projectWriter.ToString();
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
index 5d7b53c65a3e..1eebfddaef95 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
@@ -2488,6 +2488,8 @@ public void SdkReference_VersionedSdkFirst()
[InlineData("../Lib")]
[InlineData(@"..\Lib\Lib.csproj")]
[InlineData(@"..\Lib")]
+ [InlineData("$(MSBuildProjectDirectory)/../$(LibProjectName)")]
+ [InlineData(@"$(MSBuildProjectDirectory)/../Lib\$(LibProjectName).csproj")]
public void ProjectReference(string arg)
{
var testInstance = _testAssetsManager.CreateTestDirectory();
@@ -2516,64 +2518,148 @@ public class LibClass
File.WriteAllText(Path.Join(appDir, "Program.cs"), $"""
#:project {arg}
+ #:property LibProjectName=Lib
Console.WriteLine(Lib.LibClass.GetMessage());
""");
+ var expectedOutput = "Hello from Lib";
+
new DotnetCommand(Log, "run", "Program.cs")
.WithWorkingDirectory(appDir)
.Execute()
.Should().Pass()
- .And.HaveStdOut("Hello from Lib");
+ .And.HaveStdOut(expectedOutput);
+
+ // Running from a different working directory shouldn't affect handling of the relative project paths.
+ new DotnetCommand(Log, "run", "App/Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
}
- [Fact]
- public void ProjectReference_Errors()
+ [Theory]
+ [InlineData(null)]
+ [InlineData("app")]
+ public void ProjectReference_Errors(string? subdir)
{
var testInstance = _testAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
+ var relativeFilePath = Path.Join(subdir, "Program.cs");
+ var filePath = Path.Join(testInstance.Path, relativeFilePath);
+ Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
+ File.WriteAllText(filePath, """
#:project wrong.csproj
""");
// Project file does not exist.
- new DotnetCommand(Log, "run", "Program.cs")
+ new DotnetCommand(Log, "run", relativeFilePath)
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, FileBasedProgramsResources.InvalidProjectDirective,
- string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, "wrong.csproj"))));
+ .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective,
+ string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "wrong.csproj"))));
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
+ File.WriteAllText(filePath, """
#:project dir/
""");
// Project directory does not exist.
- new DotnetCommand(Log, "run", "Program.cs")
+ new DotnetCommand(Log, "run", relativeFilePath)
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, FileBasedProgramsResources.InvalidProjectDirective,
- string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, "dir/"))));
+ .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective,
+ string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "dir/"))));
- Directory.CreateDirectory(Path.Join(testInstance.Path, "dir"));
+ Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir"));
// Directory exists but has no project file.
- new DotnetCommand(Log, "run", "Program.cs")
+ new DotnetCommand(Log, "run", relativeFilePath)
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, FileBasedProgramsResources.InvalidProjectDirective,
- string.Format(FileBasedProgramsResources.CouldNotFindAnyProjectInDirectory, Path.Join(testInstance.Path, "dir/"))));
+ .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective,
+ string.Format(FileBasedProgramsResources.CouldNotFindAnyProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/"))));
- File.WriteAllText(Path.Join(testInstance.Path, "dir", "proj1.csproj"), "");
- File.WriteAllText(Path.Join(testInstance.Path, "dir", "proj2.csproj"), "");
+ File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), "");
+ File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj2.csproj"), "");
// Directory exists but has multiple project files.
- new DotnetCommand(Log, "run", "Program.cs")
+ new DotnetCommand(Log, "run", relativeFilePath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective,
+ string.Format(FileBasedProgramsResources.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/"))));
+
+ // Malformed MSBuild variable syntax.
+ File.WriteAllText(filePath, """
+ #:project $(Test
+ """);
+
+ new DotnetCommand(Log, "run", relativeFilePath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective,
+ string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "$(Test"))));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("app")]
+ public void ProjectReference_Duplicate(string? subdir)
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var relativeFilePath = Path.Join(subdir, "Program.cs");
+ var filePath = Path.Join(testInstance.Path, relativeFilePath);
+ Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
+ Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir"));
+ File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), $"""
+
+
+ {ToolsetInfo.CurrentTargetFramework}
+
+
+ """);
+
+ File.WriteAllText(filePath, """
+ #:project dir/
+ #:project dir/
+ Console.WriteLine("Hello");
+ """);
+
+ new DotnetCommand(Log, "run", relativeFilePath)
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, FileBasedProgramsResources.InvalidProjectDirective,
- string.Format(FileBasedProgramsResources.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, "dir/"))));
+ .And.HaveStdErrContaining(DirectiveError(filePath, 2, FileBasedProgramsResources.DuplicateDirective, "#:project dir/"));
+
+ File.WriteAllText(filePath, """
+ #:project dir/
+ #:project dir/proj1.csproj
+ Console.WriteLine("Hello");
+ """);
+
+ // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference
+ new DotnetCommand(Log, "run", relativeFilePath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut("Hello");
+
+ File.WriteAllText(filePath, """
+ #:project dir/
+ #:project $(MSBuildProjectDirectory)/dir/
+ Console.WriteLine("Hello");
+ """);
+
+ // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference
+ new DotnetCommand(Log, "run", relativeFilePath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut("Hello");
}
[Theory] // https://github.com/dotnet/aspnetcore/issues/63440
diff --git a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
index edaf986abdfa..db164163b90c 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
@@ -115,8 +115,8 @@ public void CountProjectReferences_FileBasedApp_CountsDirectives()
{
// Arrange
var directives = ImmutableArray.Create(
- new CSharpDirective.Project(default) { Name = "../lib/Library.csproj" },
- new CSharpDirective.Project(default) { Name = "../common/Common.csproj" }
+ new CSharpDirective.Project(default, "../lib/Library.csproj"),
+ new CSharpDirective.Project(default, "../common/Common.csproj")
);
// Act