/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Tools/BuildBoss/CompilerNuGetCheckerUtil.cs
448 строк
22 KB
Jason Malinowski
Add tests in CI that our NuGet packages can reliabily install
06 авг 2026, 22:41
06 авг 2026, 22:41
5ab4907
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Collections.Generic; using System.Diagnostics; 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; using Newtonsoft.Json.Linq; namespace BuildBoss { /// <summary> /// Verifies the contents of our toolset NuPkg and SWR files are correct. /// /// The compiler toolset is a particularly difficult package to get correct. In essence it is /// merging the output of three different exes into a single directory. That causes a number /// of issues during pack time: /// /// - The dependencies are not necessarily equal between all exes /// - The dependencies can change based on subtle changes to the code /// - There is no project which is guaranteed to have a superset of dependencies /// - There is no syntax for using the union of DLLs in a NuSpec file /// /// The most straightforward solution that could be decided on was to manage the list of dependencies /// by hand in the NuSpec file and then rigorously verify the solution here. /// </summary> internal sealed class PackageContentsChecker : ICheckerUtil { private sealed class PackagePartData { public PackagePart PackagePart { get; } public string Name { get; } public string RelativeName { get; } public string Checksum { get; } public PackagePartData(PackagePart part, string checksum) { Name = part.GetName(); RelativeName = part.GetRelativeName(); PackagePart = part; Checksum = checksum; } public override string ToString() => RelativeName; } internal static StringComparer PathComparer { get; } = StringComparer.OrdinalIgnoreCase; internal static StringComparison PathComparison { get; } = StringComparison.OrdinalIgnoreCase; internal string ArtifactsDirectory { get; } internal string Configuration { get; } internal string RepositoryDirectory { get; } internal PackageContentsChecker(string repositoryDirectory, string artifactsDirectory, string configuration) { RepositoryDirectory = repositoryDirectory; ArtifactsDirectory = artifactsDirectory; Configuration = configuration; } public bool Check(TextWriter textWriter) { try { var allGood = true; allGood &= CheckPublishData(textWriter); allGood &= CheckPackages(textWriter); allGood &= CheckExternalApis(textWriter); return allGood; } catch (Exception ex) { textWriter.WriteLine($"Error verifying: {ex.Message}"); return false; } } /// <summary> /// Verify PublishData.json contains feeds for all packages that will be published. /// </summary> private bool CheckPublishData(TextWriter textWriter) { var allGood = true; // Load PublishData.json var publishDataPath = Path.Combine(RepositoryDirectory, "eng", "config", "PublishData.json"); var publishDataRoot = JObject.Parse(File.ReadAllText(publishDataPath)); var publishDataPackages = publishDataRoot["packages"] as JObject; // Check all shipping packages have an entry in PublishData.json var regex = new Regex(@"^(.*?)\.\d.*\.nupkg$"); var packagesDirectory = Path.Combine(ArtifactsDirectory, "packages", Configuration, "Shipping"); foreach (var packageFullPath in Directory.EnumerateFiles(packagesDirectory, "*.nupkg")) { var packageFileName = Path.GetFileName(packageFullPath); var match = regex.Match(packageFileName); if (!match.Success) { allGood = false; textWriter.WriteLine($"Unexpected package file name '{packageFileName}'"); } else { var packageId = match.Groups[1].Value; if (!publishDataPackages.ContainsKey(packageId)) { allGood = false; textWriter.WriteLine($"Package doesn't have corresponding PublishData.json entry: {packageId} ({packageFileName})"); } } } return allGood; } /// <summary> /// Verify the contents of the compiler packages match the expected input /// </summary> private bool CheckPackages(TextWriter textWriter) { var allGood = true; // The VS.Tools.Roslyn package is a bit of a historical artifact from how our files used to // be laid out in the VS repository. The structure is flat which means the build assets are // mixed in with package artifacts and that makes the verification a bit more complicated and // more needs to be excluded // // The one to call out is excluding csc.exe for validation. That is because it's custom stamped // as an x86 exe to work around an issue in the VS build system // // https://github.com/dotnet/roslyn/issues/17864 allGood &= VerifyPackageCore( textWriter, SharedUtil.FindNuGetPackage(Path.Combine(ArtifactsDirectory, "VSSetup", Configuration, "DevDivPackages"), "VS.Tools.Roslyn"), excludeFunc: relativeFileName => PathComparer.Equals(relativeFileName, "csc.exe") || PathComparer.Equals(relativeFileName, "Icon.png") || PathComparer.Equals(relativeFileName, "Init.cmd") || PathComparer.Equals(relativeFileName, "VS.Tools.Roslyn.nuspec") || PathComparer.Equals(relativeFileName, "vbc.exe") || relativeFileName.EndsWith(".resources.dll", PathComparison) || relativeFileName.EndsWith(".rels", PathComparison) || relativeFileName.EndsWith(".psmdcp", PathComparison), ("", GetProjectOutputDirectory("csc", "net472")), ("", GetProjectOutputDirectory("vbc", "net472")), ("", GetProjectOutputDirectory("csi", "net472")), ("", GetProjectOutputDirectory("VBCSCompiler", "net472")), ("", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net472"))); allGood &= VerifyPackageCore( textWriter, SharedUtil.FindNuGetPackage(Path.Combine(ArtifactsDirectory, "packages", Configuration, "Shipping"), "Microsoft.Net.Compilers.Toolset.Arm64"), (@"tasks\net472", GetProjectOutputDirectory("csc-arm64", "net472")), (@"tasks\net472", GetProjectOutputDirectory("vbc-arm64", "net472")), (@"tasks\net472", GetProjectOutputDirectory("csi", "net472")), (@"tasks\net472", GetProjectOutputDirectory("VBCSCompiler-arm64", "net472")), (@"tasks\net472", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net472"))); ; allGood &= VerifyPackageCore( textWriter, SharedUtil.FindNuGetPackage(Path.Combine(ArtifactsDirectory, "packages", Configuration, "Shipping"), "Microsoft.Net.Compilers.Toolset.Framework"), (@"tasks\net472", GetProjectOutputDirectory("csc", "net472")), (@"tasks\net472", GetProjectOutputDirectory("vbc", "net472")), (@"tasks\net472", GetProjectOutputDirectory("csi", "net472")), (@"tasks\net472", GetProjectOutputDirectory("VBCSCompiler", "net472")), (@"tasks\net472", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net472"))); ; allGood &= VerifyPackageCore( textWriter, SharedUtil.FindNuGetPackage(Path.Combine(ArtifactsDirectory, "packages", Configuration, "Shipping"), "Microsoft.Net.Compilers.Toolset"), excludeFunc: relativeFileName => relativeFileName.StartsWith(@"tasks\netcore\bincore\Microsoft.DiaSymReader.Native", PathComparison) || relativeFileName.StartsWith(@"tasks\netcore\bincore\Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.dll", PathComparison) || (relativeFileName.StartsWith(@"tasks\netcore\binfx\", PathComparison) && relativeFileName.EndsWith(".targets", PathComparison)) || relativeFileName.Equals(@"tasks\netcore\bincore\csc.exe", PathComparison) || relativeFileName.Equals(@"tasks\netcore\bincore\vbc.exe", PathComparison) || relativeFileName.Equals(@"tasks\netcore\bincore\VBCSCompiler.exe", PathComparison), (@"tasks\net472", GetProjectOutputDirectory("csc", "net472")), (@"tasks\net472", GetProjectOutputDirectory("vbc", "net472")), (@"tasks\net472", GetProjectOutputDirectory("csi", "net472")), (@"tasks\net472", GetProjectOutputDirectory("VBCSCompiler", "net472")), (@"tasks\net472", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net472")), (@"tasks\netcore\bincore", GetProjectPublishDirectory("csc", "net10.0")), (@"tasks\netcore\bincore", GetProjectPublishDirectory("vbc", "net10.0")), (@"tasks\netcore\bincore", GetProjectPublishDirectory("VBCSCompiler", "net10.0")), (@"tasks\netcore", GetProjectPublishDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net10.0")), (@"tasks\netcore\binfx", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis.Sdk", "net472"))); foreach (var arch in new[] { "x86", "x64", "arm64" }) { var suffix = arch == "arm64" ? "-arm64" : ""; allGood &= VerifyPackageCore( textWriter, FindVsix($"Microsoft.CodeAnalysis.Compilers.{arch}"), (@"Contents\MSBuild\Current\Bin\Roslyn", GetProjectOutputDirectory($"csc{suffix}", "net472")), (@"Contents\MSBuild\Current\Bin\Roslyn", GetProjectOutputDirectory($"vbc{suffix}", "net472")), (@"Contents\MSBuild\Current\Bin\Roslyn", GetProjectOutputDirectory("csi", "net472")), (@"Contents\MSBuild\Current\Bin\Roslyn", GetProjectOutputDirectory($"VBCSCompiler{suffix}", "net472")), (@"Contents\MSBuild\Current\Bin\Roslyn", GetProjectOutputDirectory("Microsoft.Build.Tasks.CodeAnalysis", "net472"))); ; } return allGood; } private string GetProjectOutputDirectory(string projectName, string tfm) => Path.Combine(ArtifactsDirectory, "bin", projectName, Configuration, tfm); private string GetProjectPublishDirectory(string projectName, string tfm) => Path.Combine(ArtifactsDirectory, "bin", projectName, Configuration, tfm, "publish"); private static bool VerifyPackageCore( TextWriter textWriter, string packageFilePath, params (string PackageFolderRelativePath, string BuildOutputFolder)[] packageInputs) => VerifyPackageCore( textWriter, packageFilePath, static _ => false, packageInputs); private static bool VerifyPackageCore( TextWriter textWriter, string packageFilePath, Func<string, bool> excludeFunc, params (string PackageFolderRelativePath, string BuildOutputDirectory)[] packageInputs) { textWriter.WriteLine($"Verifying {packageFilePath}"); using var package = Package.Open(packageFilePath, FileMode.Open, FileAccess.Read); var allGood = true; var partList = GetPackagePartDataList(package); var partMap = partList.ToDictionary(x => x.RelativeName); var foundSet = new HashSet<string>(PathComparer); // First ensure all of the files in the build output directories is included in the // correct folder in the package file foreach (var tuple in packageInputs) { var buildAssets = Directory .EnumerateFiles(tuple.BuildOutputDirectory, "*.*", SearchOption.AllDirectories) .Where(IsTrackedAsset); var folderRelativePath = tuple.PackageFolderRelativePath; foreach (var buildAssetFilePath in buildAssets) { var buildAssetRelativePath = buildAssetFilePath.Substring(tuple.BuildOutputDirectory.Length + 1); buildAssetRelativePath = Path.Combine(folderRelativePath, buildAssetRelativePath); if (excludeFunc(buildAssetRelativePath)) { continue; } if (!partMap.TryGetValue(buildAssetRelativePath, out var partData)) { allGood = false; textWriter.WriteLine($"\tPart {buildAssetRelativePath} missing from package"); continue; } foundSet.Add(buildAssetRelativePath); var buildAssetChecksum = GetChecksum(buildAssetFilePath); if (buildAssetChecksum != partData.Checksum) { allGood = false; textWriter.WriteLine($"\tPart {buildAssetFilePath} has wrong checksum in package"); textWriter.WriteLine($"\t\tBuild output {buildAssetFilePath}"); textWriter.WriteLine($"\t\tPackage part {partData.Checksum}"); continue; } } } // Sanity check to make sure that we didn't accidentall include a series of empty directories if (foundSet.Count < 5) { allGood = false; textWriter.WriteLine($"Found {foundSet.Count} items in package which is far less than expected"); } // Next ensure that all of the files in the package folders were expected (aka they // came from the build output) foreach (var packageFolder in packageInputs.Select(x => x.PackageFolderRelativePath).Distinct().OrderBy(x => x)) { foreach (var partData in partList) { if (excludeFunc(partData.RelativeName)) { continue; } if (partData.RelativeName.StartsWith(packageFolder, PathComparison) && !foundSet.Contains(partData.RelativeName)) { textWriter.WriteLine($"\tFound unexpected part {partData.RelativeName}"); allGood = false; } } } return allGood; } private static List<PackagePartData> GetPackagePartDataList(Package package) { var list = new List<PackagePartData>(); foreach (var part in package.GetParts()) { var relativeName = part.GetRelativeName(); if (string.IsNullOrEmpty(relativeName)) { continue; } using var stream = part.GetStream(FileMode.Open, FileAccess.Read); var checksum = GetChecksum(stream); list.Add(new PackagePartData(part, checksum)); } return list; } private static string GetChecksum(string filePath) { using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); return GetChecksum(stream); } private static string GetChecksum(Stream stream) { using var hash = SHA256.Create(); return BitConverter.ToString(hash.ComputeHash(stream)); } /// <summary> /// Get all of the parts in the specified folder. Will exclude all items in child folders. /// </summary> private static IEnumerable<PackagePart> GetPartsInFolder(Package package, string folderRelativePath) { Debug.Assert(string.IsNullOrEmpty(folderRelativePath) || folderRelativePath[0] != '\\'); var list = GetPackagePartDataList(package); return list .Where(x => x.RelativeName.StartsWith(folderRelativePath, PathComparison)) .Select(x => x.PackagePart); } /// <summary> /// 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. /// </summary> /// <param name="textWriter"></param> private bool CheckExternalApis(TextWriter textWriter) { var packageFilePath = SharedUtil.FindNuGetPackage(Path.Combine(ArtifactsDirectory, "VSSetup", Configuration, "DevDivPackages"), "VS.ExternalAPIs.Roslyn"); var allGood = true; // This tracks the packages which are included in separate packages. Hence they don't need to // be included here. var excludedNameSet = new HashSet<string>(PathComparer) { "Microsoft.CodeAnalysis.Elfie" }; textWriter.WriteLine("Verifying contents of VS.ExternalAPIs.Roslyn"); textWriter.WriteLine("\tRoot Folder"); verifyFolder(""); return allGood; void verifyFolder(string folderRelativeName) { var foundDllNameSet = new HashSet<string>(PathComparer); var neededDllNameSet = new HashSet<string>(PathComparer); using var package = Package.Open(packageFilePath, FileMode.Open, FileAccess.Read); foreach (var part in GetPartsInFolder(package, folderRelativeName)) { var name = part.GetName(); if (Path.GetExtension(name) is not (".dll" or ".exe")) { 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 // heuristic 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) && !excludedNameSet.Contains(name)) { textWriter.WriteLine($"\t\tMissing dependency {name}"); allGood = false; } } } } private string FindVsix(string fileName) { fileName = fileName + ".vsix"; var directory = Path.Combine(ArtifactsDirectory, "VSSetup", Configuration); var file = Directory.EnumerateFiles(directory, fileName, SearchOption.AllDirectories).SingleOrDefault(); return file ?? throw new Exception($"Unable to find '{fileName}' in '{directory}'"); } /// <summary> /// The set of files that we track as assets in the NuPkg / VSIX files /// </summary> private static bool IsTrackedAsset(string filePath) { return filePath.EndsWith(".exe", PathComparison) || filePath.EndsWith(".dll", PathComparison) || filePath.EndsWith(".targets", PathComparison) || filePath.EndsWith(".config", PathComparison) || filePath.EndsWith(".rsp", PathComparison) || filePath.EndsWith(".deps.json", PathComparison) || filePath.EndsWith(".runtimeconfig.json", PathComparison) || filePath.EndsWith(".props", PathComparison); } } }