diff --git a/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs b/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs index e0b8d31..98df57c 100644 --- a/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs +++ b/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Xunit; namespace Microsoft.VisualStudio.SlnGen.UnitTests @@ -106,7 +107,7 @@ public void CustomConfigurationAndPlatforms() string solutionFilePath = GetTempFileName(); - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -193,7 +194,7 @@ public void CustomConfigurationAndPlatformsWithAlwaysBuildDisabled() string solutionFilePath = GetTempFileName(); - slnFile.Save(solutionFilePath, useFolders: false, alwaysBuild: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger(), alwaysBuild: false); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -232,7 +233,7 @@ public void CustomConfigurationAndPlatforms_IgnoresInvalidValues() string solutionFilePath = GetTempFileName(); - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -314,7 +315,7 @@ public void CustomConfigurationAndPlatforms_MapsAnyCPU() string solutionFilePath = GetTempFileName(); - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -354,7 +355,7 @@ public void ExistingSolutionIsReused() slnFile.AddProjects(new[] { project }); - slnFile.Save(path, useFolders: false); + slnFile.Save(path, useFolders: false, new TestLogger()); SlnFile.TryParseExistingSolution(path, out Guid solutionGuid, out _).ShouldBeTrue(); @@ -554,7 +555,7 @@ public void ProjectConfigurationPlatformOrderingSameAsProjects() string path = Path.GetTempFileName(); - slnFile.Save(path, useFolders: false); + slnFile.Save(path, useFolders: false, new TestLogger()); string directoryName = new DirectoryInfo(TestRootPath).Name; @@ -636,7 +637,7 @@ public void ProjectSolutionFolders() slnFile.AddProjects(projects, new Dictionary(), projects[1].FullPath); slnFile.AddSolutionItems(solutionItems); - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); SolutionFile s = SolutionFile.Parse(solutionFilePath); @@ -672,7 +673,7 @@ public void SaveToCustomLocationCreatesDirectory() SlnFile slnFile = new SlnFile(); - slnFile.Save(fullPath, useFolders: false); + slnFile.Save(fullPath, useFolders: false, new TestLogger()); File.Exists(fullPath).ShouldBeTrue(); } @@ -891,7 +892,7 @@ public void WithFoldersDoNotIgnoreMainProject() slnFile.AddProjects(projects, new Dictionary(), projects[1].FullPath); slnFile.AddSolutionItems(solutionItems); - slnFile.Save(solutionFilePath, true); + slnFile.Save(solutionFilePath, true, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -949,7 +950,7 @@ public void WithFoldersIgnoreMainProject() slnFile.AddProjects(projects, new Dictionary()); slnFile.AddSolutionItems(solutionItems); - slnFile.Save(solutionFilePath, true); + slnFile.Save(solutionFilePath, true, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -1016,7 +1017,7 @@ public void WithFoldersDoesNotCreateRootFolder(bool ignoreMainProject, bool coll slnFile.AddProjects(projects, new Dictionary(), ignoreMainProject ? null : projects[1].FullPath); slnFile.AddSolutionItems(solutionItems); - slnFile.Save(solutionFilePath, useFolders: true, collapseFolders: collapseFolders); + slnFile.Save(solutionFilePath, useFolders: true, new TestLogger(), collapseFolders: collapseFolders); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); @@ -1072,7 +1073,7 @@ public void VisualStudioVersionIsWritten() SolutionGuid = new Guid("{6370DE27-36B7-44AE-B47A-1ECF4A6D740A}"), }; - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); File.ReadAllText(solutionFilePath).ShouldBe( @"Microsoft Visual Studio Solution File, Format Version 12.00 @@ -1109,7 +1110,7 @@ public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpec slnFile.AddSolutionItems("docs", new[] { Path.Combine(this.TestRootPath, "README.md") }); // Act - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); // Assert File.ReadAllText(solutionFilePath).ShouldBe( @@ -1135,6 +1136,40 @@ public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpec StringCompareShould.IgnoreLineEndings); } + [Fact] + public void EmitWarningForProjectsOnMultipleDrives() + { + bool isWindowsPlatform = Utility.RunningOnWindows; + SlnProject projectA = new () + { + Name = "ProjectA", + FullPath = isWindowsPlatform ? @"A:\ProjectA\ProjectA.vcxitems" : "/dev/ProjectA/ProjectA.vcxitems", + ProjectGuid = new Guid("C95D800E-F016-4167-8E1B-1D3FF94CE2E2"), + ProjectTypeGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"), + }; + + SlnProject projectB = new () + { + Name = "ProjectB", + FullPath = isWindowsPlatform ? @"B:\ProjectB\ProjectB.vcxitems" : "/mnt/ProjectB/ProjectB.vcxitems", + ProjectGuid = new Guid("EAD108BE-AC70-41E6-A8C3-450C545FDC0E"), + ProjectTypeGuid = new Guid("F38341C3-343F-421A-AE68-94CD9ADCD32F"), + }; + + TestLogger logger = new (); + SlnFile slnFile = new (); + SlnProject[] projects = new[] { projectA, projectB }; + string solutionFilePath = @$"X:\{Path.GetRandomFileName()}"; + StringBuilderTextWriter writer = new (new StringBuilder(), new List()); + + slnFile.AddProjects(projects); + slnFile.Save(solutionFilePath, writer, useFolders: true, logger); + + logger.Errors.Count.ShouldBe(0); + logger.Warnings.Count.ShouldBe(1); + logger.Warnings.FirstOrDefault().Message.ShouldContain("Detected folder on a different drive from the root solution path"); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -1155,7 +1190,7 @@ public void SlnProject_IsBuildable_ReflectedAsProjectConfigurationInSolutionIncl }; slnFile.AddProjects(new[] { slnProject }); - slnFile.Save(solutionFilePath, useFolders: false); + slnFile.Save(solutionFilePath, useFolders: false, new TestLogger()); ValidateProjectInSolution( (slnProject, projectInSolution) => @@ -1178,7 +1213,7 @@ private void ValidateProjectInSolution(Action cus SlnFile slnFile = new SlnFile(); slnFile.AddProjects(projects); - slnFile.Save(solutionFilePath, useFolders); + slnFile.Save(solutionFilePath, useFolders, new TestLogger()); SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath); diff --git a/src/Microsoft.VisualStudio.SlnGen.UnitTests/StringBuilderTextWriter.cs b/src/Microsoft.VisualStudio.SlnGen.UnitTests/StringBuilderTextWriter.cs new file mode 100644 index 0000000..2c9d182 --- /dev/null +++ b/src/Microsoft.VisualStudio.SlnGen.UnitTests/StringBuilderTextWriter.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Microsoft.VisualStudio.SlnGen.UnitTests +{ + /// + /// Extends for unit tests to capture writing text. + /// + public class StringBuilderTextWriter : TextWriter + { + private readonly List _lines; + private readonly StringBuilder _lineStringBuilder = new StringBuilder(); + private readonly StringBuilder _stringBuilder; + + public StringBuilderTextWriter(StringBuilder stringBuilder, List lines) + { + _stringBuilder = stringBuilder; + _lines = lines; + } + + public override Encoding Encoding => Encoding.Unicode; + + public override void Write(char value) + { + _lineStringBuilder.Append(value); + + _stringBuilder.Append(value); + + if (value == '\n') + { + _lines.Add(_lineStringBuilder.ToString()); + + _lineStringBuilder.Clear(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.SlnGen.UnitTests/TestConsole.cs b/src/Microsoft.VisualStudio.SlnGen.UnitTests/TestConsole.cs index c70be0a..28bdc5b 100644 --- a/src/Microsoft.VisualStudio.SlnGen.UnitTests/TestConsole.cs +++ b/src/Microsoft.VisualStudio.SlnGen.UnitTests/TestConsole.cs @@ -63,34 +63,5 @@ public event ConsoleCancelEventHandler CancelKeyPress public void ResetColor() { } - - private class StringBuilderTextWriter : TextWriter - { - private readonly List _lines; - private readonly StringBuilder _lineStringBuilder = new StringBuilder(); - private readonly StringBuilder _stringBuilder; - - public StringBuilderTextWriter(StringBuilder stringBuilder, List lines) - { - _stringBuilder = stringBuilder; - _lines = lines; - } - - public override Encoding Encoding => Encoding.Unicode; - - public override void Write(char value) - { - _lineStringBuilder.Append(value); - - _stringBuilder.Append(value); - - if (value == '\n') - { - _lines.Add(_lineStringBuilder.ToString()); - - _lineStringBuilder.Clear(); - } - } - } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs b/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs index b8d2e6e..9362441 100644 --- a/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs +++ b/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs @@ -213,7 +213,7 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int if (!logger.HasLoggedErrors) { - solution.Save(solutionFileFullPath, arguments.EnableFolders(), arguments.EnableCollapseFolders(), arguments.EnableAlwaysBuild()); + solution.Save(solutionFileFullPath, arguments.EnableFolders(), logger, arguments.EnableCollapseFolders(), arguments.EnableAlwaysBuild()); } return (solutionFileFullPath, customProjectTypeGuids.Count, solutionItems.Count, solution.SolutionGuid); @@ -362,9 +362,10 @@ public void AddSolutionItems(string folderPath, IEnumerable items) /// /// The full path to the file to write to. /// Specifies if folders should be created. + /// A to use for logging. /// An optional value indicating whether or not folders containing a single item should be collapsed into their parent folder. /// An optional value indicating whether or not to always include the project in the build even if it has no matching configuration. - public void Save(string path, bool useFolders, bool collapseFolders = false, bool alwaysBuild = true) + public void Save(string path, bool useFolders, ISlnGenLogger logger, bool collapseFolders = false, bool alwaysBuild = true) { string directoryName = Path.GetDirectoryName(path); @@ -377,7 +378,7 @@ public void Save(string path, bool useFolders, bool collapseFolders = false, boo using StreamWriter writer = new StreamWriter(fileStream, Encoding.UTF8); - Save(path, writer, useFolders, collapseFolders, alwaysBuild); + Save(path, writer, useFolders, logger, collapseFolders, alwaysBuild); } /// @@ -386,9 +387,10 @@ public void Save(string path, bool useFolders, bool collapseFolders = false, boo /// A root path for the solution to make other paths relative to. /// The to save the solution file to. /// Specifies if folders should be created. + /// A to use for logging. /// An optional value indicating whether or not folders containing a single item should be collapsed into their parent folder. /// An optional value indicating whether or not to always include the project in the build even if it has no matching configuration. - internal void Save(string rootPath, TextWriter writer, bool useFolders, bool collapseFolders = false, bool alwaysBuild = true) + internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenLogger logger, bool collapseFolders = false, bool alwaysBuild = true) { writer.WriteLine(Header, _fileFormatVersion); @@ -400,7 +402,6 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, bool col } List sortedProjects = _projects.OrderBy(i => i.IsMainProject ? 0 : 1).ThenBy(i => i.FullPath).ToList(); - foreach (SlnProject project in sortedProjects) { string solutionPath = project.FullPath.ToRelativePath(rootPath).ToSolutionPath(); @@ -441,9 +442,27 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, bool col if (hierarchy != null) { + bool logDriveWarning = false; + string rootPathDrive = Path.GetPathRoot(rootPath); foreach (SlnFolder folder in hierarchy.Folders) { - string projectSolutionPath = (useFolders ? folder.FullPath.ToRelativePath(rootPath) : folder.FullPath).ToSolutionPath(); + bool useSeparateDrive = false; + bool hasFullPath = !string.IsNullOrEmpty(folder.FullPath); + if (hasFullPath) + { + string folderPathDrive = Path.GetPathRoot(folder.FullPath); + if (!string.Equals(rootPathDrive, folderPathDrive, StringComparison.OrdinalIgnoreCase)) + { + useSeparateDrive = true; + if (!logDriveWarning) + { + logger.LogWarning($"Detected folder on a different drive from the root solution path {rootPath}. This folder should not be committed to source control since it does not contain a simple, relative path and is not guaranteed to work across machines."); + logDriveWarning = true; + } + } + } + + string projectSolutionPath = (useFolders && !useSeparateDrive && hasFullPath ? folder.FullPath.ToRelativePath(rootPath) : folder.FullPath).ToSolutionPath(); // Try to preserve the folder GUID if a matching relative folder path was parsed from an existing solution if (ExistingProjectGuids != null && ExistingProjectGuids.TryGetValue(projectSolutionPath, out Guid projectGuid)) diff --git a/src/Microsoft.VisualStudio.SlnGen/SlnFolder.cs b/src/Microsoft.VisualStudio.SlnGen/SlnFolder.cs index e1d1665..9289543 100644 --- a/src/Microsoft.VisualStudio.SlnGen/SlnFolder.cs +++ b/src/Microsoft.VisualStudio.SlnGen/SlnFolder.cs @@ -35,7 +35,7 @@ public SlnFolder(string path) } /// - /// Gets the of the folder. + /// Gets or sets the of the folder. /// public Guid FolderGuid { get; set; }