Browse Source

Make PortablePdbWriter an instance type with an embed-source option

The portable-PDB writer carried its knobs (no-logo, pdb id, progress,
progress title) as a growing list of optional WritePdb parameters.
Turn the type into a configured instance whose options are properties,
and add EmbedSourceFiles (default true): when a PDB is generated next
to a project export whose .cs are already on disk, embedding the source
again is redundant, so the caller can turn it off. The per-document
checksum/hash is computed either way, so documents still resolve.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
d89bf8589b
  1. 72
      ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs
  2. 65
      ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs
  3. 2
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  4. 2
      ILSpy/Commands/GeneratePdbContextMenuEntry.cs

72
ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
@ -64,7 +65,8 @@ namespace ICSharpCode.Decompiler.Tests @@ -64,7 +65,8 @@ namespace ICSharpCode.Decompiler.Tests
using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, nameof(CustomPdbId) + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
pdbStream.SetLength(0);
PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true, pdbId: expectedPdbId);
new PortablePdbWriter { NoLogo = true, PdbId = expectedPdbId }
.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream);
pdbStream.Position = 0;
var metadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
@ -75,6 +77,68 @@ namespace ICSharpCode.Decompiler.Tests @@ -75,6 +77,68 @@ namespace ICSharpCode.Decompiler.Tests
}
}
[Test]
public void EmbedSourceFiles_False_Omits_Embedded_Source()
{
// Generating a PDB alongside a project export (where the .cs are already on disk) should be
// able to skip embedding the source again. EmbedSourceFiles = false must drop the
// EmbeddedSource custom debug info (smaller PDB, no embeddedSourceLength) while keeping the
// document checksum/hash intact.
// Compile HelloWorld's source into a uniquely-named assembly so this test never collides
// with the HelloWorld fixture's .expected files under the fixture's parallel scope.
string sourceXml = Path.Combine(TestCasePath, nameof(HelloWorld) + ".xml");
var files = XDocument.Parse(File.ReadAllText(sourceXml))
.Descendants("file").ToDictionary(f => f.Attribute("name").Value, f => f.Value);
string outputBase = Path.Combine(TestCasePath, nameof(EmbedSourceFiles_False_Omits_Embedded_Source) + ".expected");
Tester.CompileCSharpWithPdb(outputBase, files);
string peFileName = outputBase + ".dll";
var module = new PEFile(peFileName);
var resolver = new UniversalAssemblyResolver(peFileName, false,
module.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage);
byte[] WritePdbBytes(bool embedSources)
{
var decompiler = new CSharpDecompiler(module, resolver, new DecompilerSettings());
using var ms = new MemoryStream();
new PortablePdbWriter { NoLogo = true, EmbedSourceFiles = embedSources }
.WritePdb(module, decompiler, new DecompilerSettings(), ms);
return ms.ToArray();
}
// Inspect the portable PDB metadata directly (environment-independent): count the
// EmbeddedSource custom-debug-information rows and the source documents.
(int EmbeddedSources, int Documents) Inspect(byte[] pdb)
{
using var ms = new MemoryStream(pdb);
var reader = MetadataReaderProvider.FromPortablePdbStream(ms).GetMetadataReader();
int embedded = 0;
foreach (var handle in reader.CustomDebugInformation)
{
var cdi = reader.GetCustomDebugInformation(handle);
if (reader.GetGuid(cdi.Kind) == KnownGuids.EmbeddedSource)
embedded++;
}
return (embedded, reader.Documents.Count);
}
var withEmbed = WritePdbBytes(embedSources: true);
var withoutEmbed = WritePdbBytes(embedSources: false);
var embedInfo = Inspect(withEmbed);
var noEmbedInfo = Inspect(withoutEmbed);
Assert.That(embedInfo.EmbeddedSources, Is.GreaterThan(0),
"the default (embed = true) keeps the embedded-source blobs");
Assert.That(noEmbedInfo.EmbeddedSources, Is.EqualTo(0),
"embed = false must omit every embedded-source blob");
Assert.That(noEmbedInfo.Documents, Is.EqualTo(embedInfo.Documents).And.GreaterThan(0),
"the source documents (with their checksum/hash) must remain even without embedded source");
Assert.That(withoutEmbed.Length, Is.LessThan(withEmbed.Length),
"omitting embedded source must produce a strictly smaller PDB");
}
[Test]
public void ProgressReporting()
{
@ -104,7 +168,8 @@ namespace ICSharpCode.Decompiler.Tests @@ -104,7 +168,8 @@ namespace ICSharpCode.Decompiler.Tests
using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, nameof(ProgressReporting) + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
pdbStream.SetLength(0);
PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true, progress: new TestProgressReporter(reportFunc));
new PortablePdbWriter { NoLogo = true, Progress = new TestProgressReporter(reportFunc) }
.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream);
pdbStream.Position = 0;
var metadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
@ -142,7 +207,8 @@ namespace ICSharpCode.Decompiler.Tests @@ -142,7 +207,8 @@ namespace ICSharpCode.Decompiler.Tests
using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, testName + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
pdbStream.SetLength(0);
PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true);
new PortablePdbWriter { NoLogo = true }
.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream);
pdbStream.Position = 0;
using (Stream peStream = File.OpenRead(peFileName))
using (Stream expectedPdbStream = File.OpenRead(pdbFileName))

65
ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs

@ -45,6 +45,29 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -45,6 +45,29 @@ namespace ICSharpCode.Decompiler.DebugInfo
{
const string decompilerVersion = DecompilerVersionInfo.Version;
/// <summary>Suppresses the "generated by ICSharpCode.Decompiler" header comment in each source file.</summary>
public bool NoLogo { get; set; }
/// <summary>
/// The PDB id (GUID + stamp) to stamp into the generated PDB. When <c>null</c> (the default) the
/// id is taken from the assembly's CodeView debug-directory entry so the PDB matches the PE.
/// </summary>
public BlobContentId? PdbId { get; set; }
/// <summary>Optional per-file progress reporter.</summary>
public IProgress<DecompilationProgress> Progress { get; set; }
/// <summary>The title surfaced through <see cref="Progress"/>.</summary>
public string CurrentProgressTitle { get; set; } = "Generating portable PDB...";
/// <summary>
/// When <c>true</c> (the default) each decompiled source file is embedded into the PDB. Set to
/// <c>false</c> when the source is already written to disk (e.g. alongside a project export) so
/// the PDB stays small and references the on-disk files instead. The per-document checksum/hash
/// is written either way.
/// </summary>
public bool EmbedSourceFiles { get; set; } = true;
public static bool HasCodeViewDebugDirectoryEntry(PEFile file)
{
return file != null && file.Reader.ReadDebugDirectory().Any(entry => entry.Type == DebugDirectoryEntryType.CodeView);
@ -65,16 +88,13 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -65,16 +88,13 @@ namespace ICSharpCode.Decompiler.DebugInfo
return true;
}
public static void WritePdb(
public void WritePdb(
PEFile file,
CSharpDecompiler decompiler,
DecompilerSettings settings,
Stream targetStream,
bool noLogo = false,
BlobContentId? pdbId = null,
IProgress<DecompilationProgress> progress = null,
string currentProgressTitle = "Generating portable PDB...")
Stream targetStream)
{
BlobContentId? pdbId = PdbId;
MetadataBuilder metadata = new MetadataBuilder();
MetadataReader reader = file.Metadata;
var entrypointHandle = MetadataTokens.MethodDefinitionHandle(file.Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress);
@ -100,7 +120,7 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -100,7 +120,7 @@ namespace ICSharpCode.Decompiler.DebugInfo
DecompilationProgress currentProgress = new() {
TotalUnits = sourceFiles.Count,
UnitsCompleted = 0,
Title = currentProgressTitle
Title = CurrentProgressTitle
};
foreach (var sourceFile in sourceFiles)
@ -108,17 +128,17 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -108,17 +128,17 @@ namespace ICSharpCode.Decompiler.DebugInfo
// Generate syntax tree
var syntaxTree = decompiler.DecompileTypes(sourceFile);
if (progress != null)
if (Progress != null)
{
currentProgress.UnitsCompleted++;
progress.Report(currentProgress);
Progress.Report(currentProgress);
}
if (!syntaxTree.HasChildren)
continue;
// Generate source and checksum
if (!noLogo)
if (!NoLogo)
syntaxTree.InsertChildAfter(null, new Comment(" PDB and source generated by ICSharpCode.Decompiler " + decompilerVersion), Roles.Comment);
var sourceText = SyntaxTreeToString(syntaxTree, settings);
@ -131,7 +151,8 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -131,7 +151,8 @@ namespace ICSharpCode.Decompiler.DebugInfo
lock (metadata)
{
var sourceBlob = WriteSourceToBlob(metadata, sourceText, out var sourceCheckSum);
// The document hash is always required; the source blob is only embedded when asked.
var sourceCheckSum = ComputeSourceChecksum(sourceText);
var name = metadata.GetOrAddDocumentName(sourceFile.Key);
// Create Document(Handle)
@ -141,9 +162,12 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -141,9 +162,12 @@ namespace ICSharpCode.Decompiler.DebugInfo
language: metadata.GetOrAddGuid(KnownGuids.CSharpLanguageGuid));
// Add embedded source to the PDB
customDebugInfo.Add((document,
metadata.GetOrAddGuid(KnownGuids.EmbeddedSource),
sourceBlob));
if (EmbedSourceFiles)
{
customDebugInfo.Add((document,
metadata.GetOrAddGuid(KnownGuids.EmbeddedSource),
WriteSourceToBlob(metadata, sourceText)));
}
debugInfoGen.GenerateImportScopes(metadata, globalImportScope);
@ -291,7 +315,14 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -291,7 +315,14 @@ namespace ICSharpCode.Decompiler.DebugInfo
return builder;
}
static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText, out byte[] sourceCheckSum)
static byte[] ComputeSourceChecksum(string sourceText)
{
byte[] bytes = Encoding.UTF8.GetBytes(sourceText);
using var hasher = SHA256.Create();
return hasher.ComputeHash(bytes);
}
static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText)
{
var builder = new BlobBuilder();
using (var memory = new MemoryStream())
@ -303,10 +334,6 @@ namespace ICSharpCode.Decompiler.DebugInfo @@ -303,10 +334,6 @@ namespace ICSharpCode.Decompiler.DebugInfo
byte[] buffer = memory.ToArray();
builder.WriteInt32(bytes.Length); // compressed
builder.WriteBytes(buffer);
using (var hasher = SHA256.Create())
{
sourceCheckSum = hasher.ComputeHash(bytes);
}
}
return metadata.GetOrAddBlob(builder);

2
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -613,7 +613,7 @@ Examples: @@ -613,7 +613,7 @@ Examples:
using (FileStream stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write))
{
var decompiler = GetDecompiler(assemblyFileName);
PortablePdbWriter.WritePdb(module, decompiler, GetSettings(module), stream);
new PortablePdbWriter().WritePdb(module, decompiler, GetSettings(module), stream);
}
return 0;

2
ILSpy/Commands/GeneratePdbContextMenuEntry.cs

@ -125,7 +125,7 @@ namespace ILSpy.Commands @@ -125,7 +125,7 @@ namespace ILSpy.Commands
var decompiler = new CSharpDecompiler(file, resolver, settings) {
CancellationToken = token,
};
PortablePdbWriter.WritePdb(file, decompiler, settings, stream);
new PortablePdbWriter().WritePdb(file, decompiler, settings, stream);
output.Write(string.Format(Resources.GeneratedPDBFile, pdbFileName));
output.WriteLine();
}

Loading…
Cancel
Save