diff --git a/src/NuGetUtility/NuGetUtility.csproj b/src/NuGetUtility/NuGetUtility.csproj index ffa0a9ce..12705054 100644 --- a/src/NuGetUtility/NuGetUtility.csproj +++ b/src/NuGetUtility/NuGetUtility.csproj @@ -21,7 +21,7 @@ - 10.0 + 12.0 diff --git a/src/NuGetUtility/Program.cs b/src/NuGetUtility/Program.cs index 58572a1d..90348156 100644 --- a/src/NuGetUtility/Program.cs +++ b/src/NuGetUtility/Program.cs @@ -23,6 +23,7 @@ using NuGetUtility.Wrapper.NuGetWrapper.ProjectModel; using NuGetUtility.Wrapper.NuGetWrapper.Protocol; using NuGetUtility.Wrapper.NuGetWrapper.Protocol.Core.Types; +using NuGetUtility.Wrapper.SolutionPersistenceWrapper; namespace NuGetUtility { @@ -119,8 +120,9 @@ private async Task OnExecuteAsync(CancellationToken cancellationToken) IFileDownloader urlLicenseFileDownloader = GetFileDownloader(httpClient); IOutputFormatter output = GetOutputFormatter(); - MsBuildAbstraction msBuild = new MsBuildAbstraction(); - var projectCollector = new ProjectsCollector(msBuild); + var solutionPersistance = new SolutionPersistanceWrapper(); + var projectCollector = new ProjectsCollector(solutionPersistance); + var msBuild = new MsBuildAbstraction(); var projectReader = new ReferencedPackageReader(msBuild, new LockFileFactory(), GetPackagesConfigReader()); var validator = new LicenseValidator.LicenseValidator(licenseMappings, allowedLicenses, diff --git a/src/NuGetUtility/ReferencedPackagesReader/ProjectsCollector.cs b/src/NuGetUtility/ReferencedPackagesReader/ProjectsCollector.cs index 2f58ec7a..7ab18c32 100644 --- a/src/NuGetUtility/ReferencedPackagesReader/ProjectsCollector.cs +++ b/src/NuGetUtility/ReferencedPackagesReader/ProjectsCollector.cs @@ -1,23 +1,23 @@ // Licensed to the projects contributors. // The license conditions are provided in the LICENSE file located in the project root -using NuGetUtility.Wrapper.MsBuildWrapper; +using NuGetUtility.Wrapper.SolutionPersistenceWrapper; namespace NuGetUtility.ReferencedPackagesReader { public class ProjectsCollector { - private readonly IMsBuildAbstraction _msBuild; - public ProjectsCollector(IMsBuildAbstraction msBuild) + private readonly ISolutionPersistanceWrapper _solutionPersistance; + public ProjectsCollector(ISolutionPersistanceWrapper solutionPersistance) { - _msBuild = msBuild; + _solutionPersistance = solutionPersistance; } public async Task> GetProjectsAsync(string inputPath) { return Path.GetExtension(inputPath).StartsWith(".sln") - ? (await _msBuild.GetProjectsFromSolutionAsync(Path.GetFullPath(inputPath))).Where(File.Exists).Select(Path.GetFullPath) - : new[] { Path.GetFullPath(inputPath) }; + ? (await _solutionPersistance.GetProjectsFromSolutionAsync(Path.GetFullPath(inputPath))).Where(File.Exists).Select(Path.GetFullPath) + : [Path.GetFullPath(inputPath)]; } } } diff --git a/src/NuGetUtility/Wrapper/MsBuildWrapper/IMsBuildAbstraction.cs b/src/NuGetUtility/Wrapper/MsBuildWrapper/IMsBuildAbstraction.cs index 384c3db3..dea21a9e 100644 --- a/src/NuGetUtility/Wrapper/MsBuildWrapper/IMsBuildAbstraction.cs +++ b/src/NuGetUtility/Wrapper/MsBuildWrapper/IMsBuildAbstraction.cs @@ -6,6 +6,5 @@ namespace NuGetUtility.Wrapper.MsBuildWrapper public interface IMsBuildAbstraction { IProject GetProject(string projectPath); - Task> GetProjectsFromSolutionAsync(string inputPath); } } diff --git a/src/NuGetUtility/Wrapper/MsBuildWrapper/MsBuildAbstraction.cs b/src/NuGetUtility/Wrapper/MsBuildWrapper/MsBuildAbstraction.cs index 6ee33379..0168052e 100644 --- a/src/NuGetUtility/Wrapper/MsBuildWrapper/MsBuildAbstraction.cs +++ b/src/NuGetUtility/Wrapper/MsBuildWrapper/MsBuildAbstraction.cs @@ -3,8 +3,6 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Locator; -using Microsoft.VisualStudio.SolutionPersistence; -using Microsoft.VisualStudio.SolutionPersistence.Serializer; namespace NuGetUtility.Wrapper.MsBuildWrapper { @@ -33,14 +31,6 @@ public IProject GetProject(string projectPath) return new ProjectWrapper(project); } - public async Task> GetProjectsFromSolutionAsync(string inputPath) - { - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(inputPath) ?? throw new MsBuildAbstractionException("Failed to determine serializer for solution"); - - Microsoft.VisualStudio.SolutionPersistence.Model.SolutionModel model = await serializer.OpenAsync(inputPath, CancellationToken.None); - return model.SolutionProjects.Select(p => p.FilePath); - } - private static void RegisterMsBuildLocatorIfNeeded() { if (!MSBuildLocator.IsRegistered) diff --git a/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/ISolutionPersistanceWrapper.cs b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/ISolutionPersistanceWrapper.cs new file mode 100644 index 00000000..b8ea1512 --- /dev/null +++ b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/ISolutionPersistanceWrapper.cs @@ -0,0 +1,10 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace NuGetUtility.Wrapper.SolutionPersistenceWrapper +{ + public interface ISolutionPersistanceWrapper + { + Task> GetProjectsFromSolutionAsync(string inputPath); + } +} diff --git a/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceException.cs b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceException.cs new file mode 100644 index 00000000..429d6632 --- /dev/null +++ b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceException.cs @@ -0,0 +1,14 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace NuGetUtility.Wrapper.SolutionPersistenceWrapper +{ + public class SolutionPersistanceException : Exception + { + public SolutionPersistanceException(string message) + : base(message) { } + + public SolutionPersistanceException(string message, Exception inner) + : base(message, inner) { } + } +} diff --git a/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceWrapper.cs b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceWrapper.cs new file mode 100644 index 00000000..11cc8802 --- /dev/null +++ b/src/NuGetUtility/Wrapper/SolutionPersistenceWrapper/SolutionPersistanceWrapper.cs @@ -0,0 +1,20 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using Microsoft.VisualStudio.SolutionPersistence; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; + +namespace NuGetUtility.Wrapper.SolutionPersistenceWrapper +{ + public class SolutionPersistanceWrapper : ISolutionPersistanceWrapper + { + public async Task> GetProjectsFromSolutionAsync(string inputPath) + { + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(inputPath) ?? throw new SolutionPersistanceException("Failed to determine serializer for solution"); + + Microsoft.VisualStudio.SolutionPersistence.Model.SolutionModel model = await serializer.OpenAsync(inputPath, CancellationToken.None); + string? solutionPath = Path.GetDirectoryName(inputPath); + return model.SolutionProjects.Select(p => solutionPath is null ? p.FilePath : Path.Combine(solutionPath, p.FilePath)); + } + } +} diff --git a/tests/NuGetUtility.Test/ReferencedPackagesReader/ProjectsCollectorTest.cs b/tests/NuGetUtility.Test/ReferencedPackagesReader/ProjectsCollectorTest.cs index b023b63f..5e0c44d7 100644 --- a/tests/NuGetUtility.Test/ReferencedPackagesReader/ProjectsCollectorTest.cs +++ b/tests/NuGetUtility.Test/ReferencedPackagesReader/ProjectsCollectorTest.cs @@ -5,7 +5,7 @@ using NSubstitute; using NuGetUtility.ReferencedPackagesReader; using NuGetUtility.Test.Helper.ShuffelledEnumerable; -using NuGetUtility.Wrapper.MsBuildWrapper; +using NuGetUtility.Wrapper.SolutionPersistenceWrapper; namespace NuGetUtility.Test.ReferencedPackagesReader { @@ -22,10 +22,10 @@ public ProjectsCollectorTest() public void SetUp() { _fixture = new Fixture(); - _msBuild = Substitute.For(); - _uut = new ProjectsCollector(_msBuild); + _solutionPersistanceWrapper = Substitute.For(); + _uut = new ProjectsCollector(_solutionPersistanceWrapper); } - private IMsBuildAbstraction _msBuild = null!; + private ISolutionPersistanceWrapper _solutionPersistanceWrapper = null!; private ProjectsCollector _uut = null!; private Fixture _fixture = null!; private readonly VerifySettings _osPlatformSpecificVerifySettings; @@ -38,7 +38,7 @@ public async Task GetProjects_Should_ReturnProjectsAsListDirectly(string project { IEnumerable result = await _uut.GetProjectsAsync(projectFile); Assert.That(result, Is.EqualTo(new[] { Path.GetFullPath(projectFile) })); - await _msBuild.DidNotReceive().GetProjectsFromSolutionAsync(Arg.Any()); + await _solutionPersistanceWrapper.DidNotReceive().GetProjectsFromSolutionAsync(Arg.Any()); } [TestCase("A.sln")] @@ -49,7 +49,7 @@ public async Task GetProjects_Should_QueryMsBuildToGetProjectsForSolutionFiles(s { _ = await _uut.GetProjectsAsync(solutionFile); - await _msBuild.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); + await _solutionPersistanceWrapper.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); } [TestCase("A.sln")] @@ -58,12 +58,12 @@ public async Task GetProjects_Should_QueryMsBuildToGetProjectsForSolutionFiles(s [TestCase("C.slnx")] public async Task GetProjects_Should_ReturnEmptyArray_If_SolutionContainsNoProjects(string solutionFile) { - _msBuild.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); + _solutionPersistanceWrapper.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); IEnumerable result = await _uut.GetProjectsAsync(solutionFile); Assert.That(result, Is.Empty); - await _msBuild.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); + await _solutionPersistanceWrapper.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); } [TestCase("A.sln")] @@ -73,12 +73,12 @@ public async Task GetProjects_Should_ReturnEmptyArray_If_SolutionContainsNoProje public async Task GetProjects_Should_ReturnEmptyArray_If_SolutionContainsProjectsThatDontExist(string solutionFile) { IEnumerable projects = _fixture.CreateMany(); - _msBuild.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult(projects)); + _solutionPersistanceWrapper.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult(projects)); IEnumerable result = await _uut.GetProjectsAsync(solutionFile); Assert.That(result, Is.Empty); - await _msBuild.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); + await _solutionPersistanceWrapper.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); } [TestCase("A.sln")] @@ -89,12 +89,12 @@ public async Task GetProjects_Should_ReturnArrayOfProjects_If_SolutionContainsPr { string[] projects = _fixture.CreateMany().ToArray(); CreateFiles(projects); - _msBuild.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult>(projects)); + _solutionPersistanceWrapper.GetProjectsFromSolutionAsync(Arg.Any()).Returns(Task.FromResult>(projects)); IEnumerable result = await _uut.GetProjectsAsync(solutionFile); Assert.That(result, Is.EqualTo(projects.Select(Path.GetFullPath))); - await _msBuild.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); + await _solutionPersistanceWrapper.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); } [TestCase("A.sln")] @@ -108,29 +108,39 @@ public async Task GetProjects_Should_ReturnOnlyExistingProjectsInSolutionFile(st CreateFiles(existingProjects); - _msBuild.GetProjectsFromSolutionAsync(Arg.Any()) + _solutionPersistanceWrapper.GetProjectsFromSolutionAsync(Arg.Any()) .Returns(existingProjects.Concat(missingProjects).Shuffle(54321)); IEnumerable result = await _uut.GetProjectsAsync(solutionFile); Assert.That(result, Is.EquivalentTo(existingProjects.Select(Path.GetFullPath))); - await _msBuild.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); + await _solutionPersistanceWrapper.Received(1).GetProjectsFromSolutionAsync(Path.GetFullPath(solutionFile)); } [Test] public async Task GetProjectsFromSolution_Should_ReturnProjectsInActualSolutionFileRelativePath() { - var msbuild = new MsBuildAbstraction(); - IEnumerable result = await msbuild.GetProjectsFromSolutionAsync("../../../../targets/Projects.sln"); - await Verify(string.Join(",", result), _osPlatformSpecificVerifySettings); + var solutionPersistance = new SolutionPersistanceWrapper(); + string solutionFolder = Path.GetFullPath("../../../../targets"); + string solutionFileName = "Projects.sln"; + IEnumerable result = await solutionPersistance.GetProjectsFromSolutionAsync(Path.Combine(solutionFolder, solutionFileName)); + + Assert.That(result.Select(Path.IsPathRooted), Is.All.True); + + await Verify(string.Join(",", result.Select(p => GetPathRelativeTo(solutionFolder, p))), _osPlatformSpecificVerifySettings); } [Test] public async Task GetProjectsFromXmlSolution_Should_ReturnProjectsInActualSolutionFileRelativePath() { - var msbuild = new MsBuildAbstraction(); - IEnumerable result = await msbuild.GetProjectsFromSolutionAsync("../../../../targets/slnx/slnx.slnx"); - await Verify(string.Join(",", result), _osPlatformSpecificVerifySettings); + var solutionPersistance = new SolutionPersistanceWrapper(); + string solutionFolder = Path.GetFullPath("../../../../targets/slnx"); + string solutionFileName = "slnx.slnx"; + IEnumerable result = await solutionPersistance.GetProjectsFromSolutionAsync(Path.Combine(solutionFolder, solutionFileName)); + + Assert.That(result.Select(Path.IsPathRooted), Is.All.True); + + await Verify(string.Join(",", result.Select(p => GetPathRelativeTo(solutionFolder, p))), _osPlatformSpecificVerifySettings); } private static void CreateFiles(IEnumerable files) @@ -140,5 +150,24 @@ private static void CreateFiles(IEnumerable files) File.WriteAllBytes(file, Array.Empty()); } } + + private static string GetPathRelativeTo(string relativeTo, string path) +#if NETFRAMEWORK + { + // Require trailing backslash for path + relativeTo = relativeTo.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + relativeTo += Path.DirectorySeparatorChar; + + Uri baseUri = new Uri(relativeTo); + Uri fullUri = new Uri(path); + + Uri relativeUri = baseUri.MakeRelativeUri(fullUri); + + return relativeUri.ToString().Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + } +#else + => Path.GetRelativePath(relativeTo, path); +#endif } }