diff --git a/src/Tools/BuildBoss/BuildBoss.csproj b/src/Tools/BuildBoss/BuildBoss.csproj index 18e025bffe5567e124ab103fcb3e09d4fcbf0b2d..e531135abe8aea8b0dad4946ed177e4bfeb4effe 100644 --- a/src/Tools/BuildBoss/BuildBoss.csproj +++ b/src/Tools/BuildBoss/BuildBoss.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Tools/BuildBoss/CompilerNuGetCheckerUtil.cs b/src/Tools/BuildBoss/CompilerNuGetCheckerUtil.cs index 0e81d015b0d6cf734be9b918b55a3e27659c5f90..88eb579291ed145a6f0fed6b737c6c6f9d378816 100644 --- a/src/Tools/BuildBoss/CompilerNuGetCheckerUtil.cs +++ b/src/Tools/BuildBoss/CompilerNuGetCheckerUtil.cs @@ -4,6 +4,8 @@ using System.IO; using System.IO.Packaging; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using System.Security.Cryptography; using System.Text.RegularExpressions; @@ -74,6 +76,7 @@ public bool Check(TextWriter textWriter) allGood &= CheckDesktop(textWriter, filter(isDesktop: true)); allGood &= CheckCoreClr(textWriter, filter(isDesktop: false)); allGood &= CheckCombined(textWriter, packageAssets); + allGood &= CheckExternalApis(textWriter); return allGood; IEnumerable filter(bool isDesktop) => packageAssets.Where(x => x.IsDesktop == isDesktop).Select(x => x.FileRelativeName); @@ -147,6 +150,74 @@ private bool CheckCombined(TextWriter textWriter, IEnumerable pack list); } + /// + /// Verifies the VS.ExternalAPIs.Roslyn package is self consistent. Need to ensure that we insert all of the project dependencies + /// that we build into the package. If we miss a dependency then the VS insertion will fail. Big refactorings can often forget to + /// properly update this package. + /// + /// + /// + private bool CheckExternalApis(TextWriter textWriter) + { + var packageFilePath = FindNuGetPackage(Path.Combine(ArtifactsDirectory, "VSSetup", Configuration, "DevDivPackages"), "VS.ExternalAPIs.Roslyn"); + var allGood = true; + + textWriter.WriteLine("Verifying contents of VS.ExternalAPIs.Roslyn"); + textWriter.WriteLine("\tRoot Folder"); + verifyFolder(""); + textWriter.WriteLine("\tRemote Debugger net20"); + verifyFolder(@"RemoteDebugger\net20"); + textWriter.WriteLine("\tRemote Debugger net50"); + verifyFolder(@"RemoteDebugger\net50"); + return allGood; + + void verifyFolder(string folderRelativeName) + { + var foundDllNameSet = new HashSet(PathComparer); + var neededDllNameSet = new HashSet(PathComparer); + foreach (var part in GetPartsInFolder(packageFilePath, "")) + { + var name = part.GetName(); + if (Path.GetExtension(name) != ".dll") + { + continue; + } + + foundDllNameSet.Add(Path.GetFileNameWithoutExtension(name)); + using var peReader = new PEReader(part.GetStream(FileMode.Open, FileAccess.Read)); + var metadataReader = peReader.GetMetadataReader(); + foreach (var handle in metadataReader.AssemblyReferences) + { + var assemblyReference = metadataReader.GetAssemblyReference(handle); + var assemblyName = metadataReader.GetString(assemblyReference.Name); + neededDllNameSet.Add(assemblyName); + } + } + + if (foundDllNameSet.Count == 0) + { + allGood = false; + textWriter.WriteLine($"\t\tFound zero DLLs in {folderRelativeName}"); + return; + } + + // As a simplification we only validate the assembly names that begin with Microsoft.CodeAnalysis. This is a good + // hueristic for finding assemblies that we build. Can be expanded in the future if we find more assemblies that + // are worth validating here. + var neededDllNames = neededDllNameSet + .Where(x => x.StartsWith("Microsoft.CodeAnalysis")) + .OrderBy(x => x, PathComparer); + foreach (var name in neededDllNames) + { + if (!foundDllNameSet.Contains(name)) + { + textWriter.WriteLine($"\t\tMissing dependency {name}"); + allGood = false; + } + } + } + } + private bool GetPackageAssets(TextWriter textWriter, List packageAssets) { var allGood = true; @@ -300,40 +371,6 @@ IEnumerable enumerateFiles() string folderRelativePath, IEnumerable dllFileNames) { - Debug.Assert(string.IsNullOrEmpty(folderRelativePath) || folderRelativePath[0] != '\\'); - - // Get all of the assets parts that are in the specified folder. Will exclude items that - // are in any child folder - IEnumerable getPartsInFolder() - { - using (var package = Package.Open(packageFilePath, FileMode.Open, FileAccess.Read)) - { - foreach (var part in package.GetParts()) - { - var relativeName = part.Uri.ToString().Replace('/', '\\'); - if (string.IsNullOrEmpty(relativeName)) - { - continue; - } - - if (relativeName[0] == '\\') - { - relativeName = relativeName.Substring(1); - } - - if (!relativeName.StartsWith(folderRelativePath, PathComparison)) - { - continue; - } - - if (IsTrackedAsset(relativeName)) - { - yield return relativeName; - } - } - } - } - var map = dllFileNames .ToDictionary( keySelector: x => Path.Combine(folderRelativePath, x), @@ -343,8 +380,14 @@ IEnumerable getPartsInFolder() var packageFileName = Path.GetFileName(packageFilePath); textWriter.WriteLine($"Verifying {packageFileName}"); - foreach (var relativeName in getPartsInFolder()) + foreach (var part in GetPartsInFolder(packageFilePath, folderRelativePath)) { + var relativeName = part.GetRelativeName(); + if (!IsTrackedAsset(relativeName)) + { + continue; + } + var name = Path.GetFileName(relativeName); if (map.TryGetValue(relativeName, out var isFound)) { @@ -374,6 +417,33 @@ IEnumerable getPartsInFolder() return allGood; } + /// + /// Get all of the parts in the specified folder. Will exclude all items in child folders. + /// + private static IEnumerable GetPartsInFolder(string packageFilePath, string folderRelativePath) + { + Debug.Assert(string.IsNullOrEmpty(folderRelativePath) || folderRelativePath[0] != '\\'); + + using (var package = Package.Open(packageFilePath, FileMode.Open, FileAccess.Read)) + { + foreach (var part in package.GetParts()) + { + var relativeName = part.GetRelativeName(); + if (string.IsNullOrEmpty(relativeName)) + { + continue; + } + + if (!relativeName.StartsWith(folderRelativePath, PathComparison)) + { + continue; + } + + yield return part; + } + } + } + private string FindNuGetPackage(string directory, string partialName) { var regex = $@"{partialName}.\d.*\.nupkg"; diff --git a/src/Tools/BuildBoss/Extensions.cs b/src/Tools/BuildBoss/Extensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..f0911449279e3cb7e2303fde9b3b2d55b9818455 --- /dev/null +++ b/src/Tools/BuildBoss/Extensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuildBoss +{ + internal static class Extensions + { + internal static string GetRelativeName(this PackagePart part) + { + var relativeName = part.Uri.ToString().Replace('/', '\\'); + if (!string.IsNullOrEmpty(relativeName) && relativeName[0] == '\\') + { + relativeName = relativeName.Substring(1); + } + + return relativeName; + } + + internal static string GetName(this PackagePart part) => Path.GetFileName(GetRelativeName(part)); + } +} diff --git a/src/Tools/BuildBoss/Program.cs b/src/Tools/BuildBoss/Program.cs index 41efe1360e1ab02598e1a2f69671123b4cce8685..ac0139902bea4384c6fa3c25860ea025fde3eab7 100644 --- a/src/Tools/BuildBoss/Program.cs +++ b/src/Tools/BuildBoss/Program.cs @@ -96,9 +96,9 @@ private static bool Go(string repositoryDirectory, string configuration, List