diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs index e2cd47a2169f2..8bada428d0f98 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VirtualProject.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; +using System.Text; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -21,6 +24,45 @@ internal static class VirtualCSharpFileBasedProgramProject internal static string GetVirtualProjectPath(string documentFilePath) => Path.ChangeExtension(documentFilePath, ".csproj"); + #region TODO: Copy-pasted from dotnet run-api. Delete when run-api is adopted. + // See https://github.com/dotnet/sdk/blob/b5dbc69cc28676ac6ea615654c8016a11b75e747/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs#L10 + private static class Sha256Hasher + { + public static string Hash(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); + byte[] hash = SHA256.HashData(bytes); +#if NET9_0_OR_GREATER + return Convert.ToHexStringLower(hash); +#else + return Convert.ToHexString(hash).ToLowerInvariant(); +#endif + } + + public static string HashWithNormalizedCasing(string text) + { + return Hash(text.ToUpperInvariant()); + } + } + + // TODO: this is a copy of SDK run-api code. Must delete when adopting run-api. + // See https://github.com/dotnet/sdk/blob/5a4292947487a9d34f4256c1d17fb3dc26859174/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs#L449 + internal static string GetArtifactsPath(string entryPointFileFullPath) + { + // 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", directoryName); + } + #endregion + internal static (string virtualProjectXml, bool isFileBasedProgram) MakeVirtualProjectContent(string documentFilePath, SourceText text) { Contract.ThrowIfFalse(PathUtilities.IsAbsolute(documentFilePath)); @@ -30,48 +72,74 @@ internal static (string virtualProjectXml, bool isFileBasedProgram) MakeVirtualP var root = tree.GetRoot(); var isFileBasedProgram = root.GetLeadingTrivia().Any(SyntaxKind.IgnoredDirectiveTrivia) || root.ChildNodes().Any(node => node.IsKind(SyntaxKind.GlobalStatement)); + var artifactsPath = GetArtifactsPath(documentFilePath); + + var targetFramework = Environment.GetEnvironmentVariable("DOTNET_RUN_FILE_TFM") ?? "net10.0"; + var virtualProjectXml = $""" - + + + + false + {SecurityElement.Escape(artifactsPath)} + - - Exe - net8.0 - enable - enable - $(Features);FileBasedProgram - false - + + + + Exe + {SecurityElement.Escape(targetFramework)} + enable + enable + + + + false + + + + preview + + + + $(Features);FileBasedProgram + + + + + + + + + + + + + + + + + + - + <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> + + + + + - - - - - - - - - - - <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> - - - - - - """;