private static bool ValidatePdb(FileWithDebugData input, Stream pdbStream, List <PackageFile> noSourceLink, List <(PackageFile file, string errors)> sourceLinkErrors,
private async Task CalculateValidity(IReadOnlyList <PackageFile> files) { var filesWithPdb = (from pf in files let ext = Path.GetExtension(pf.Path) where ".dll".Equals(ext, StringComparison.OrdinalIgnoreCase) || ".exe".Equals(ext, StringComparison.OrdinalIgnoreCase) || ".winmd".Equals(ext, StringComparison.OrdinalIgnoreCase) select new FileWithPdb { Primary = pf, Pdb = pf.GetAssociatedFiles().FirstOrDefault(af => ".pdb".Equals(Path.GetExtension(af.Path), StringComparison.OrdinalIgnoreCase)) }) .ToList(); var sourceLinkErrors = new List <(PackageFile file, string errors)>(); var noSourceLink = new List <PackageFile>(); var noSymbols = new List <FileWithDebugData>(); var untrackedSources = new List <FileWithDebugData>(); var nonDeterministic = new List <PackageFile>(); var allFilePaths = filesWithPdb.ToDictionary(pf => pf.Primary.Path); var pdbChecksumValid = true; foreach (var file in filesWithPdb.ToArray()) // work on array as we'll remove items that are satellite assemblies as we go { // Skip satellite assemblies if (IsSatelliteAssembly(file.Primary.Path)) { filesWithPdb.Remove(allFilePaths[file.Primary.Path]); continue; } // If we have a PDB, try loading that first. If not, may be embedded. // Local checks first if (file.Pdb != null) { var filePair = new FileWithDebugData(file.Primary, null); if (!ValidatePdb(filePair, file.Pdb.GetStream(), noSourceLink, sourceLinkErrors, untrackedSources, nonDeterministic)) { pdbChecksumValid = false; noSymbols.Add(filePair); } } else // No PDB, see if it's embedded { try { using var str = file.Primary.GetStream(); using var tempFile = new TemporaryFile(str); var assemblyMetadata = AssemblyMetadataReader.ReadMetaData(tempFile.FileName); if (assemblyMetadata?.DebugData.HasDebugInfo == true) { // we have an embedded pdb if (!assemblyMetadata.DebugData.HasSourceLink) { noSourceLink.Add(file.Primary); } if (assemblyMetadata.DebugData.SourceLinkErrors.Count > 0) { // Has source link errors sourceLinkErrors.Add((file.Primary, string.Join("\n", assemblyMetadata.DebugData.SourceLinkErrors))); } // Check for non-embedded sources if (assemblyMetadata.DebugData.UntrackedSources.Count > 0) { var filePair = new FileWithDebugData(file.Primary, assemblyMetadata.DebugData); untrackedSources.Add(filePair); } // Check for deterministic sources if (!assemblyMetadata.DebugData.SourcesAreDeterministic) { nonDeterministic.Add(file.Primary); } } else // no embedded pdb, try to look for it elsewhere { noSymbols.Add(new FileWithDebugData(file.Primary, assemblyMetadata?.DebugData)); } } catch // an error occured, no symbols { noSymbols.Add(new FileWithDebugData(file.Primary, null)); } } } var requireExternal = false; // See if any pdb's are missing and check for a snupkg on NuGet.org. if (noSymbols.Count > 0 && _publishedOnNuGetOrg) { // try to get on NuGet.org // https://www.nuget.org/api/v2/symbolpackage/Newtonsoft.Json/12.0.3 -- Will redirect try { #pragma warning disable CA2234 // Pass system uri objects instead of strings var response = await _httpClient.GetAsync($"https://www.nuget.org/api/v2/symbolpackage/{_package.Id}/{_package.Version.ToNormalizedString()}").ConfigureAwait(false); #pragma warning restore CA2234 // Pass system uri objects instead of strings if (response.IsSuccessStatusCode) // we'll get a 404 if none { requireExternal = true; using var getStream = await response.Content.ReadAsStreamAsync(); using var tempFile = new TemporaryFile(getStream, ".snupkg"); using var package = new ZipPackage(tempFile.FileName); // Look for pdb's for the missing files var dict = package.GetFiles().ToDictionary(k => k.Path); foreach (var file in noSymbols.ToArray()) // from a copy so we can remove as we go { // file to look for var pdbpath = Path.ChangeExtension(file.File.Path, ".pdb"); if (dict.TryGetValue(pdbpath, out var pdbfile)) { // Validate if (ValidatePdb(file, pdbfile.GetStream(), noSourceLink, sourceLinkErrors, untrackedSources, nonDeterministic)) { noSymbols.Remove(file); } else { pdbChecksumValid = false; } } } } } catch // Could not check, leave status as-is { } } // Check for Microsoft assemblies on the Microsoft symbol server if (noSymbols.Count > 0) { var microsoftFiles = noSymbols.Where(f => f.DebugData != null && IsMicrosoftFile(f.File)).ToList(); foreach (var file in microsoftFiles) { var pdbStream = await GetSymbolsAsync(file.DebugData !.SymbolKeys); if (pdbStream != null) { requireExternal = true; // Found a PDB for it if (ValidatePdb(file, pdbStream, noSourceLink, sourceLinkErrors, untrackedSources, nonDeterministic)) { noSymbols.Remove(file); } else { pdbChecksumValid = false; } } } } if (noSymbols.Count == 0 && noSourceLink.Count == 0 && sourceLinkErrors.Count == 0) { if (untrackedSources.Count > 0) { SourceLinkResult = SymbolValidationResult.HasUntrackedSources; var sb = new StringBuilder("Contains untracked sources:\n"); sb.AppendLine("To Fix:"); sb.AppendLine("<EmbedUntrackedSources>true</EmbedUntrackedSources>"); foreach (var untracked in untrackedSources) { sb.AppendLine($"Assembly: {untracked.File.Path}"); foreach (var source in untracked.DebugData !.UntrackedSources) { sb.AppendLine($" {source}"); } sb.AppendLine(); } SourceLinkErrorMessage = sb.ToString(); } else if (filesWithPdb.Count == 0) { SourceLinkResult = SymbolValidationResult.NothingToValidate; SourceLinkErrorMessage = "No files found to validate"; } else if (requireExternal) { SourceLinkResult = SymbolValidationResult.ValidExternal; SourceLinkErrorMessage = null; } else { SourceLinkResult = SymbolValidationResult.Valid; SourceLinkErrorMessage = null; } } else { var found = false; var sb = new StringBuilder(); if (noSourceLink.Count > 0) { SourceLinkResult = SymbolValidationResult.NoSourceLink; sb.AppendLine($"Missing Source Link for:\n{string.Join("\n", noSourceLink.Select(p => p.Path)) }"); found = true; } if (sourceLinkErrors.Count > 0) { SourceLinkResult = SymbolValidationResult.InvalidSourceLink; if (found) { sb.AppendLine(); } foreach (var(file, errors) in sourceLinkErrors) { sb.AppendLine($"Source Link errors for {file.Path}:\n{string.Join("\n", errors) }"); } found = true; } if (noSymbols.Count > 0) // No symbols "wins" as it's more severe { SourceLinkResult = SymbolValidationResult.NoSymbols; if (found) { sb.AppendLine(); } if (!pdbChecksumValid) { sb.AppendLine("Some PDB's checksums do not match their PE files and are shown as missing."); } sb.AppendLine($"Missing Symbols for:\n{string.Join("\n", noSymbols.Select(p => p.File.Path)) }"); } SourceLinkErrorMessage = sb.ToString(); } if (SourceLinkResult == SymbolValidationResult.NothingToValidate) { DeterministicResult = DeterministicResult.NothingToValidate; DeterministicErrorMessage = null; } else if (SourceLinkResult == SymbolValidationResult.NoSymbols) { DeterministicResult = DeterministicResult.NonDeterministic; DeterministicErrorMessage = "Missing Symbols"; } else if (nonDeterministic.Count > 0) { DeterministicResult = DeterministicResult.NonDeterministic; var sb = new StringBuilder(); sb.AppendLine("Ensure that the following properties are enabled for CI builds\nand you're using at least the 2.1.300 SDK:"); sb.AppendLine("<Deterministic>true</Deterministic>"); sb.AppendLine("<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>"); sb.AppendLine(); sb.AppendLine("The following assemblies have not been compiled with deterministic settings:"); foreach (var file in nonDeterministic) { sb.AppendLine(file.Path); } DeterministicErrorMessage = sb.ToString(); } else if (SourceLinkResult == SymbolValidationResult.HasUntrackedSources) { DeterministicResult = DeterministicResult.HasUntrackedSources; DeterministicErrorMessage = null; } else { DeterministicResult = DeterministicResult.Valid; DeterministicErrorMessage = null; } }