-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Support batch PDB generation. #3619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support batch PDB generation. #3619
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds support for batch PDB generation in ILSpy, allowing users to select multiple assemblies and generate PDB files for all of them at once. When multiple assemblies are selected, users are prompted to choose a target folder, and PDB files are generated with filenames based on each assembly's short name.
Key Changes:
- Extended context menu and main menu entries to support multiple assembly selection
- Added new
GeneratePdbForAssembliesmethod for batch processing - Added localized resource strings for batch operation feedback
Reviewed Changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| ILSpy/Commands/GeneratePdbContextMenuEntry.cs | Implements batch PDB generation logic with multi-select support for both context menu and main menu entries |
| ILSpy/Properties/Resources.resx | Adds English resource strings for batch generation status messages |
| ILSpy/Properties/Resources.zh-Hans.resx | Adds Chinese translations for new resource strings |
| ILSpy/Properties/Resources.Designer.cs | Auto-generated designer code for accessing new resource strings |
Files not reviewed (1)
- ILSpy/Properties/Resources.Designer.cs: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (options.Progress != null) | ||
| { | ||
| options.Progress.Report(new DecompilationProgress { | ||
| Title = "Generating portable PDB...", |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The progress title "Generating portable PDB..." is hardcoded and not localized. Consider adding this string to the resource files for proper internationalization, similar to other user-facing messages in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonyps5201314 if you want, you can add this. There are other locations where this same string is used, that might need updating as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
71cfce3 resolved.
| output.WriteLine(); | ||
| output.WriteLine(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1")); | ||
| output.WriteLine(); | ||
| output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + targetFolder + "\""); }); |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using /select with a folder path (targetFolder) will not work as intended. The /select parameter expects a file path to select in Explorer. To open the folder directly, use:
Process.Start("explorer", "\"" + targetFolder + "\"");This will open the target folder itself, which is more appropriate for a batch operation where multiple files were generated.
| output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + targetFolder + "\""); }); | |
| output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "\"" + targetFolder + "\""); }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonyps5201314 Yes, this a legitimate bug: explorer C:\path\to\folder is the syntax for folders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1f13b80 resolved.
This also incidentally resolves another small 'unmodern' issue in ILSpy: every time 'Open or Select File/Folder' was executed, a new explorer.exe process was launched, and these processes would not automatically exit even after closing the opened windows.

|
Looks good to me! Thank you very much for contributing this feature! |
…re fully overwritten/truncated.
…spawning an `explorer.exe` process that doesn't exit automatically on every call.
…ing `OpenFolderAndSelectItem`.
…drive than the OS.
|
@siegfriedpammer I believe I have addressed all the points you mentioned. The functionality of Additionally, I intentionally did not set an initial directory for |
| @@ -0,0 +1,152 @@ | |||
| using System; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs license header
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e027a1a resolved.
ILSpy/Util/ShellHelper.cs
Outdated
| } | ||
| } | ||
| if (itemPidl != IntPtr.Zero) | ||
| CoTaskMemFree(itemPidl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, this needs try-finally too?
ILSpy/Util/ShellHelper.cs
Outdated
| } | ||
| catch | ||
| { | ||
| // ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what kind of exceptions are we ignoring here? "catch all" is considered bad pratice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#3619 (comment) answered.
ILSpy/Util/ShellHelper.cs
Outdated
| } | ||
| catch | ||
| { | ||
| // ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what kind of exceptions are we ignoring here? "catch all" is considered bad practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike C, which typically uses return values to signal errors, C# is a language that relies heavily on exceptions. Since almost any method call has the potential to throw, adding a try-catch block is certainly an effective measure to prevent the application from crashing.
These potential exceptions include Process.Start failures, DllImport("shell32.dll") errors (such as failing to load the DLL or finding the specific entry point), and even managed exceptions thrown from within P/Invoke during extreme scenarios (e.g., when hooked by tools like EasyHook).
Given that the consequence of a failure in ShellHelper's public API is merely that a folder fails to open—which is a minor impact—I believe adding a general exception handler is acceptable here. (Actually, I didn't add these catch blocks myself; I simply retained them because I agree they are necessary.)
| using System.Runtime.InteropServices; | ||
| using System.Linq; | ||
| using System.Collections.Generic; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #pragma warning disable CA1060 // Move pinvokes to native methods class | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't actually get this warning when I compiled, but I still added it as you suggested.
b631b55 resolved.
| { | ||
| if (context.SelectedTreeNodes == null) | ||
| return; | ||
| var paths = new List<string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe it would make sense to use a HashSet here? Note that this would need a StringComparer.OrdinalIgnoreCase on Windows :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think a change is strictly necessary here. If multiple instances of the same file appear in the Assembly List, it implies that the insertion logic (AssemblyList.OpenAssembly) isn't handling things correctly. The restriction should be enforced at that layer, rather than becoming a burden for internal calls.
Given that AssemblyList defines:
readonly Dictionary<string, LoadedAssembly> byFilename = new Dictionary<string, LoadedAssembly>(StringComparer.OrdinalIgnoreCase);
we can infer that ILSpy doesn't suffer from this issue. However, I did notice a few days ago that dnSpyEx indeed has a problem with duplicate file entries.
That being said, your point is valid. Since relying on external restrictions increases the cognitive load on the caller (requiring the user to pay special attention), I went ahead and handled it inside OpenFolderAndSelectItems.
752e6ed resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 17 comments.
Files not reviewed (1)
- ILSpy/Properties/Resources.Designer.cs: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| internal static void GeneratePdbForAssemblies(IEnumerable<LoadedAssembly> assemblies, LanguageService languageService, DockWorkspace dockWorkspace) | ||
| { | ||
| var assemblyArray = assemblies?.Where(a => a != null).ToArray() ?? []; | ||
| if (assemblyArray == null || assemblyArray.Length == 0) |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The redundant null check can be simplified. Line 123 already filters out null values with assemblies?.Where(a => a != null).ToArray() ?? [], so the subsequent check if (assemblyArray == null || assemblyArray.Length == 0) will never have a null assemblyArray.
| if (assemblyArray == null || assemblyArray.Length == 0) | |
| if (assemblyArray.Length == 0) |
| string msg = string.Format(Resources.CannotCreatePDBFile, ":" + Environment.NewLine + | ||
| string.Join(Environment.NewLine, unsupported.Select(u => Path.GetFileName(u.FileName))) | ||
| + Environment.NewLine); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message formatting is incorrect. The first parameter to string.Format should be just the assembly filename, not a formatted string with additional content. This will result in a malformed error message like "Cannot create PDB file for :
Assembly1.dll
Assembly2.dll
because the PE debug directory type 'CodeView' is missing."
Consider changing this to properly format the list of unsupported assemblies, for example:
string msg = Resources.CannotCreatePDBFile + Environment.NewLine +
string.Join(Environment.NewLine, unsupported.Select(u => " - " + Path.GetFileName(u.FileName)));
MessageBox.Show(msg);| string msg = string.Format(Resources.CannotCreatePDBFile, ":" + Environment.NewLine + | |
| string.Join(Environment.NewLine, unsupported.Select(u => Path.GetFileName(u.FileName))) | |
| + Environment.NewLine); | |
| string msg = Resources.CannotCreatePDBFile + Environment.NewLine + | |
| string.Join(Environment.NewLine, unsupported.Select(u => " - " + Path.GetFileName(u.FileName))); |
ILSpy/Util/ShellHelper.cs
Outdated
| IntPtr folderPidl = IntPtr.Zero; | ||
| uint attrs; | ||
| int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out attrs); | ||
| if (hr == 0 && folderPidl != IntPtr.Zero) | ||
| { | ||
| SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); | ||
| CoTaskMemFree(folderPidl); | ||
| } | ||
| else | ||
| { | ||
| // fallback | ||
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); | ||
| } |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace this call with a call to managed code if possible.
| IntPtr folderPidl = IntPtr.Zero; | |
| uint attrs; | |
| int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out attrs); | |
| if (hr == 0 && folderPidl != IntPtr.Zero) | |
| { | |
| SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); | |
| CoTaskMemFree(folderPidl); | |
| } | |
| else | |
| { | |
| // fallback | |
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); | |
| } | |
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); |
ILSpy/Util/ShellHelper.cs
Outdated
| IntPtr folderPidl = IntPtr.Zero; | ||
| uint attrs; | ||
| int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out attrs); | ||
| if (hr == 0 && folderPidl != IntPtr.Zero) | ||
| { | ||
| SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); | ||
| CoTaskMemFree(folderPidl); | ||
| } | ||
| else | ||
| { | ||
| // fallback | ||
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); | ||
| } |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace this call with a call to managed code if possible.
| IntPtr folderPidl = IntPtr.Zero; | |
| uint attrs; | |
| int hr = SHParseDisplayName(folderPath, IntPtr.Zero, out folderPidl, 0, out attrs); | |
| if (hr == 0 && folderPidl != IntPtr.Zero) | |
| { | |
| SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); | |
| CoTaskMemFree(folderPidl); | |
| } | |
| else | |
| { | |
| // fallback | |
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); | |
| } | |
| // Use managed code to open the folder in Explorer | |
| Process.Start(new ProcessStartInfo { FileName = folderPath, UseShellExecute = true }); |
| if (hr == 0 && folderPidl != IntPtr.Zero) | ||
| { | ||
| SHOpenFolderAndSelectItems(folderPidl, 0, null, 0); | ||
| CoTaskMemFree(folderPidl); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace this call with a call to managed code if possible.
| static class ShellHelper | ||
| { | ||
| [DllImport("shell32.dll", CharSet = CharSet.Unicode)] | ||
| static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); | ||
|
|
||
| [DllImport("shell32.dll")] | ||
| static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, uint dwFlags); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| [DllImport("shell32.dll")] | ||
| static extern IntPtr ILFindLastID(IntPtr pidl); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| [DllImport("shell32.dll")] | |
| static extern IntPtr ILFindLastID(IntPtr pidl); |
| static extern IntPtr ILFindLastID(IntPtr pidl); | ||
|
|
||
| [DllImport("ole32.dll")] | ||
| static extern void CoTaskMemFree(IntPtr pv); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| // Ask for target folder | ||
| var dlg = new OpenFolderDialog(); | ||
| dlg.Title = Resources.SelectPDBOutputFolder; | ||
| if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName)) |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A != true' can be simplified to '!A'.
| if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FolderName)) | |
| if (!dlg.ShowDialog() || string.IsNullOrWhiteSpace(dlg.FolderName)) |
|
@siegfriedpammer Thank you for taking the time to review the code again. I have addressed all your questions and made the necessary changes. Is there anything else that needs modification? |
…re fully overwritten/truncated.
6010c85 to
4e5727f
Compare
Added support for multi-select batch PDB generation. When multiple assemblies are selected, the user is prompted to choose a target folder, and files are generated as
{ShortName}.pdbbased on the assembly's short name.