Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Solution filter (.slnf) #2121

Merged
merged 6 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/OmniSharp.Host/Services/OmniSharpEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ public OmniSharpEnvironment(
{
TargetDirectory = path;
}
else if (File.Exists(path) && Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase))
else if (File.Exists(path) && (Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(path).Equals(".slnf", StringComparison.OrdinalIgnoreCase)))
{
SolutionFilePath = path;
TargetDirectory = Path.GetDirectoryName(path);
}

if (TargetDirectory == null)
{
throw new ArgumentException("OmniSharp only supports being launched with a directory path or a path to a solution (.sln) file.", nameof(path));
throw new ArgumentException("OmniSharp only supports being launched with a directory path or a path to a solution (.sln, .slnf) file.", nameof(path));
}

HostProcessId = hostPid;
Expand Down
41 changes: 27 additions & 14 deletions src/OmniSharp.MSBuild/ProjectSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,21 @@ public void Initalize(IConfiguration configuration)
}
}

public Task WaitForIdleAsync() { return _manager.WaitForQueueEmptyAsync(); }
public Task WaitForIdleAsync() { return _manager.WaitForQueueEmptyAsync(); }

private IEnumerable<(string, ProjectIdInfo)> GetInitialProjectPathsAndIds()
{
// If a solution was provided, use it.
if (!string.IsNullOrEmpty(_environment.SolutionFilePath))
{
_solutionFileOrRootPath = _environment.SolutionFilePath;
return GetProjectPathsAndIdsFromSolution(_environment.SolutionFilePath);
return GetProjectPathsAndIdsFromSolutionOrFilter(_environment.SolutionFilePath, out _solutionFileOrRootPath);
}

// Otherwise, assume that the path provided is a directory and look for a solution there.
var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger);
if (!string.IsNullOrEmpty(solutionFilePath))
{
_solutionFileOrRootPath = solutionFilePath;
return GetProjectPathsAndIdsFromSolution(solutionFilePath);
return GetProjectPathsAndIdsFromSolutionOrFilter(solutionFilePath, out _solutionFileOrRootPath);
}

// Finally, if there isn't a single solution immediately available,
Expand All @@ -158,10 +156,20 @@ public void Initalize(IConfiguration configuration)
});
}

private IEnumerable<(string, ProjectIdInfo)> GetProjectPathsAndIdsFromSolution(string solutionFilePath)
private IEnumerable<(string, ProjectIdInfo)> GetProjectPathsAndIdsFromSolutionOrFilter(string solutionOrFilterFilePath, out string solutionFilePath)
{
_logger.LogInformation($"Detecting projects in '{solutionFilePath}'.");
_logger.LogInformation($"Detecting projects in '{solutionOrFilterFilePath}'.");

solutionFilePath = solutionOrFilterFilePath;

var projectFilter = ImmutableHashSet<string>.Empty;
if (SolutionFilterReader.IsSolutionFilterFilename(solutionOrFilterFilePath) &&
!SolutionFilterReader.TryRead(solutionOrFilterFilePath, out solutionFilePath, out projectFilter))
{
throw new InvalidSolutionFileException($"Solution filter file was invalid.");
}

var solutionFolder = Path.GetDirectoryName(solutionFilePath);
var solutionFile = SolutionFile.ParseFile(solutionFilePath);
var processedProjects = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var result = new List<(string, ProjectIdInfo)>();
Expand Down Expand Up @@ -196,10 +204,14 @@ public void Initalize(IConfiguration configuration)
continue;
}

// Solution files are assumed to contain relative paths to project files with Windows-style slashes.
var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath);
projectFilePath = Path.GetFullPath(projectFilePath);
// Solution files contain relative paths to project files with Windows-style slashes.
var relativeProjectfilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
var projectFilePath = Path.GetFullPath(Path.Combine(solutionFolder, relativeProjectfilePath));
if (!projectFilter.IsEmpty &&
!projectFilter.Contains(projectFilePath))
{
continue;
}

// Have we seen this project? If so, move on.
if (processedProjects.Contains(projectFilePath))
Expand All @@ -225,11 +237,12 @@ public void Initalize(IConfiguration configuration)

private static string FindSolutionFilePath(string rootPath, ILogger logger)
{
// currently, Directory.GetFiles collects files that the file extension has 'sln' prefix.
// this causes collecting unexpected files like 'x.slnx', or 'x.slnproj'.
// currently, Directory.GetFiles on Windows collects files that the file extension has 'sln' prefix, while
// GetFiles on Mono looks for an exact match. Use an approach that works for both.
// see https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netframework-4.7.2 ('Note' description)
var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln").Where(x => Path.GetExtension(x).Equals(".sln", StringComparison.OrdinalIgnoreCase)).ToArray();
var result = SolutionSelector.Pick(solutionsFilePaths, rootPath);
var solutionFiltersFilePaths = Directory.GetFiles(rootPath, "*.slnf").Where(x => Path.GetExtension(x).Equals(".slnf", StringComparison.OrdinalIgnoreCase)).ToArray();
var result = SolutionSelector.Pick(solutionsFilePaths.Concat(solutionFiltersFilePaths).ToArray(), rootPath);

if (result.Message != null)
{
Expand Down
70 changes: 70 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/SolutionFilterReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Immutable;
using System.IO;
using Newtonsoft.Json.Linq;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal static class SolutionFilterReader
{
public static bool IsSolutionFilterFilename(string filename)
{
return Path.GetExtension(filename).Equals(".slnf", StringComparison.OrdinalIgnoreCase);
}

public static bool TryRead(string filterFilename, out string solutionFilename, out ImmutableHashSet<string> projectFilter)
{
try
{
var filterDirectory = Path.GetDirectoryName(filterFilename);

var document = JObject.Parse(File.ReadAllText(filterFilename));
var solution = document["solution"];
// Convert directory separators to the platform's default, since that is what MSBuild provide us.
var solutionPath = ((string)solution?["path"])?.Replace('\\', Path.DirectorySeparatorChar);

solutionFilename = Path.GetFullPath(Path.Combine(filterDirectory, solutionPath));
if (!File.Exists(solutionFilename))
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

// The base directory for projects is the solution folder.
var solutionDirectory = Path.GetDirectoryName(solutionFilename);

var filterProjects = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
var projects = (JArray)solution?["projects"] ?? new JArray();
foreach (string project in projects)
{
// Convert directory separators to the platform's default, since that is what MSBuild provide us.
var projectPath = project?.Replace('\\', Path.DirectorySeparatorChar);
if (projectPath is null)
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

var projectFilename = Path.GetFullPath(Path.Combine(solutionDirectory, projectPath));
if (!File.Exists(projectFilename))
{
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}

// Fill the filter with the absolute project paths.
filterProjects.Add(projectFilename);
}

projectFilter = filterProjects.ToImmutable();
return true;
}
catch
{
solutionFilename = string.Empty;
projectFilter = ImmutableHashSet<string>.Empty;
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace ProjectAndSolution
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"solution": {
"path": "Solution\\ProjectAndSolutionFilter.sln",
"projects": [
"..\\Project\\ProjectAndSolutionFilter.csproj"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectAndSolutionFilter", "..\Project\ProjectAndSolutionFilter.csproj", "{A4C2694D-AEB4-4CB1-8951-5290424EF883}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x64.ActiveCfg = Debug|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x64.Build.0 = Debug|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x86.ActiveCfg = Debug|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Debug|x86.Build.0 = Debug|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|Any CPU.Build.0 = Release|Any CPU
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x64.ActiveCfg = Release|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x64.Build.0 = Release|x64
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x86.ActiveCfg = Release|x86
{A4C2694D-AEB4-4CB1-8951-5290424EF883}.Release|x86.Build.0 = Release|x86
EndGlobalSection
EndGlobal
28 changes: 28 additions & 0 deletions tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ public async Task TestProjectAndSolution()
Assert.Equal("netcoreapp3.1", targetFramework.ShortName);
}

[Fact]
public async Task TestProjectAndSolutionFilter()
{
using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectAndSolutionFilter"))
using (var host = CreateMSBuildTestHost(testProject.Directory))
{
var workspaceInfo = await host.RequestMSBuildWorkspaceInfoAsync();

Assert.Equal("ProjectAndSolutionFilter.sln", Path.GetFileName(workspaceInfo.SolutionPath));
Assert.NotNull(workspaceInfo.Projects);
var project = Assert.Single(workspaceInfo.Projects);

Assert.Equal("ProjectAndSolutionFilter", project.AssemblyName);
Assert.Equal("bin/Debug/netcoreapp2.1/", project.OutputPath.EnsureForwardSlashes());
Assert.Equal("obj/Debug/netcoreapp2.1/", project.IntermediateOutputPath.EnsureForwardSlashes());
var expectedTargetPath = $"{testProject.Directory}/Project/{project.OutputPath}ProjectAndSolutionFilter.dll".EnsureForwardSlashes();
Assert.Equal(expectedTargetPath, project.TargetPath.EnsureForwardSlashes());
Assert.Equal("Debug", project.Configuration);
Assert.Equal("AnyCPU", project.Platform);
Assert.True(project.IsExe);
Assert.False(project.IsUnityProject);

Assert.Equal(".NETCoreApp,Version=v2.1", project.TargetFramework);
var targetFramework = Assert.Single(project.TargetFrameworks);
Assert.Equal("netcoreapp2.1", targetFramework.ShortName);
}
}

[Fact]
public async Task ProjectAndSolutionWithProjectSection()
{
Expand Down