diff --git a/eng/illink.targets b/eng/illink.targets
index 962e6f49ceb78..e6bdc8f893591 100644
--- a/eng/illink.targets
+++ b/eng/illink.targets
@@ -208,6 +208,7 @@
$(ILLinkArgs) --action link $(TargetName)
$(ILLinkArgs) -b true
+ $(ILLinkArgs) --preserve-symbol-paths
$(ILLinkArgs) -x "$(ILLinkDescriptorsLibraryBuildXml)"
$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"
diff --git a/src/tools/illink/src/ILLink.Tasks/LinkTask.cs b/src/tools/illink/src/ILLink.Tasks/LinkTask.cs
index 9fd5c536603ee..ce24b35de9e48 100644
--- a/src/tools/illink/src/ILLink.Tasks/LinkTask.cs
+++ b/src/tools/illink/src/ILLink.Tasks/LinkTask.cs
@@ -194,6 +194,13 @@ public class ILLink : ToolTask
public bool RemoveSymbols { set => _removeSymbols = value; }
bool? _removeSymbols;
+ ///
+ /// Preserve original path to debug symbols from each assembly's debug header.
+ /// Maps to '--preserve-symbol-paths' if true.
+ /// Default if not specified is to write out the full path to the pdb in the debug header.
+ ///
+ public bool PreserveSymbolPaths { get; set; }
+
///
/// Sets the default action for trimmable assemblies.
/// Maps to '--trim-mode'
@@ -474,6 +481,9 @@ protected override string GenerateResponseFileCommands ()
if (_removeSymbols == false)
args.AppendLine ("-b");
+ if (PreserveSymbolPaths)
+ args.AppendLine ("--preserve-symbol-paths");
+
if (CustomSteps != null) {
foreach (var customStep in CustomSteps) {
args.Append ("--custom-step ");
diff --git a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
index d6d45f475fc33..a59527573202e 100644
--- a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
+++ b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
@@ -144,6 +144,7 @@ Copyright (c) .NET Foundation. All rights reserved.
TrimMode="$(TrimMode)"
DefaultAction="$(_TrimmerDefaultAction)"
RemoveSymbols="$(TrimmerRemoveSymbols)"
+ PreserveSymbolPaths="$(_TrimmerPreserveSymbolPaths)"
FeatureSettings="@(_TrimmerFeatureSettings)"
CustomData="@(_TrimmerCustomData)"
@@ -237,6 +238,11 @@ Copyright (c) .NET Foundation. All rights reserved.
false
+
+ <_TrimmerPreserveSymbolPaths Condition=" '$(_TrimmerPreserveSymbolPaths)' == '' and '$(DeterministicSourcePaths)' == 'true' ">true
+ <_TrimmerPreserveSymbolPaths Condition=" '$(_TrimmerPreserveSymbolPaths)' == '' ">false
+
+
diff --git a/src/tools/illink/src/linker/CompatibilitySuppressions.xml b/src/tools/illink/src/linker/CompatibilitySuppressions.xml
index 08ab3bdda1659..af38ca258f95b 100644
--- a/src/tools/illink/src/linker/CompatibilitySuppressions.xml
+++ b/src/tools/illink/src/linker/CompatibilitySuppressions.xml
@@ -1,4 +1,4 @@
-
+
@@ -1503,12 +1503,24 @@
ref/net9.0/illink.dll
lib/net9.0/illink.dll
+
+ CP0002
+ M:Mono.Linker.LinkContext.get_PreserveSymbolPaths
+ ref/net9.0/illink.dll
+ lib/net9.0/illink.dll
+
CP0002
M:Mono.Linker.LinkContext.set_KeepComInterfaces(System.Boolean)
ref/net9.0/illink.dll
lib/net9.0/illink.dll
+
+ CP0002
+ M:Mono.Linker.LinkContext.set_PreserveSymbolPaths(System.Boolean)
+ ref/net9.0/illink.dll
+ lib/net9.0/illink.dll
+
CP0008
T:Mono.Linker.LinkContext
diff --git a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs
index 2fa3c03f2b396..ccd5f42a774aa 100644
--- a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs
+++ b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs
@@ -189,6 +189,7 @@ WriterParameters SaveSymbols (AssemblyDefinition assembly)
return parameters;
parameters.WriteSymbols = true;
+ parameters.SymbolWriterProvider = new CustomSymbolWriterProvider (Context.PreserveSymbolPaths);
return parameters;
}
diff --git a/src/tools/illink/src/linker/Linker/CustomSymbolWriter.cs b/src/tools/illink/src/linker/Linker/CustomSymbolWriter.cs
new file mode 100644
index 0000000000000..2380ea294a280
--- /dev/null
+++ b/src/tools/illink/src/linker/Linker/CustomSymbolWriter.cs
@@ -0,0 +1,120 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO;
+using System.Text;
+
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace Mono.Linker
+{
+ internal sealed class CustomSymbolWriterProvider : ISymbolWriterProvider
+ {
+ readonly DefaultSymbolWriterProvider _defaultProvider = new DefaultSymbolWriterProvider ();
+ readonly bool _preserveSymbolPaths;
+
+ public CustomSymbolWriterProvider (bool preserveSymbolPaths) => this._preserveSymbolPaths = preserveSymbolPaths;
+
+ public ISymbolWriter GetSymbolWriter (ModuleDefinition module, string fileName)
+ => new CustomSymbolWriter (_defaultProvider.GetSymbolWriter (module, fileName), module, _preserveSymbolPaths);
+
+ public ISymbolWriter GetSymbolWriter (ModuleDefinition module, Stream symbolStream)
+ => new CustomSymbolWriter (_defaultProvider.GetSymbolWriter (module, symbolStream), module, _preserveSymbolPaths);
+ }
+
+ internal sealed class CustomSymbolWriter : ISymbolWriter
+ {
+ // ASCII "RSDS": https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2
+ const int CodeViewSignature = 0x53445352;
+
+ readonly ISymbolWriter _symbolWriter;
+ readonly ModuleDefinition _module;
+ readonly bool _preserveSymbolPaths;
+
+ internal CustomSymbolWriter (ISymbolWriter defaultWriter, ModuleDefinition module, bool preserveSymbolPaths)
+ {
+ _symbolWriter = defaultWriter;
+ _module = module;
+ _preserveSymbolPaths = preserveSymbolPaths;
+ }
+
+ public ImageDebugHeader GetDebugHeader ()
+ {
+ var header = _symbolWriter.GetDebugHeader ();
+ if (!_preserveSymbolPaths)
+ return header;
+
+ if (!header.HasEntries)
+ return header;
+
+ for (int i = 0; i < header.Entries.Length; i++) {
+ header.Entries [i] = ProcessEntry (header.Entries [i]);
+ }
+
+ return header;
+ }
+
+ ImageDebugHeaderEntry ProcessEntry (ImageDebugHeaderEntry entry)
+ {
+ if (entry.Directory.Type != ImageDebugType.CodeView)
+ return entry;
+
+ var reader = new BinaryReader (new MemoryStream (entry.Data));
+ var newDataStream = new MemoryStream ();
+ var writer = new BinaryWriter (newDataStream);
+
+ var sig = reader.ReadUInt32 ();
+ if (sig != CodeViewSignature)
+ return entry;
+
+ writer.Write (sig);
+ writer.Write (reader.ReadBytes (16)); // MVID
+ writer.Write (reader.ReadUInt32 ()); // Age
+
+ writer.Write (Encoding.UTF8.GetBytes (GetOriginalPdbPath ()));
+ writer.Write ((byte) 0);
+
+ var newData = newDataStream.ToArray ();
+
+ var directory = entry.Directory;
+ directory.SizeOfData = newData.Length;
+
+ return new ImageDebugHeaderEntry (directory, newData);
+ }
+
+ string GetOriginalPdbPath ()
+ {
+ if (!_module.HasDebugHeader)
+ return string.Empty;
+
+ var debugHeader = _module.GetDebugHeader ();
+ foreach (var entry in debugHeader.Entries) {
+ if (entry.Directory.Type != ImageDebugType.CodeView)
+ continue;
+
+ var reader = new BinaryReader (new MemoryStream (entry.Data));
+ var sig = reader.ReadUInt32 ();
+ if (sig != CodeViewSignature)
+ return string.Empty;
+
+ var stream = reader.BaseStream;
+ stream.Seek (16 + 4, SeekOrigin.Current); // MVID and Age
+ // Pdb path is NUL-terminated path at offset 24.
+ // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2
+ return Encoding.UTF8.GetString (
+ reader.ReadBytes ((int) (stream.Length - stream.Position - 1))); // remaining length - ending \0
+ }
+
+ return string.Empty;
+ }
+
+ public ISymbolReaderProvider GetReaderProvider () => _symbolWriter.GetReaderProvider ();
+
+ public void Write (MethodDebugInformation info) => _symbolWriter.Write (info);
+
+ public void Write () => _symbolWriter.Write ();
+
+ public void Dispose () => _symbolWriter.Dispose ();
+ }
+}
diff --git a/src/tools/illink/src/linker/Linker/Driver.cs b/src/tools/illink/src/linker/Linker/Driver.cs
index 149abe748e45e..5b0c6971a06e9 100644
--- a/src/tools/illink/src/linker/Linker/Driver.cs
+++ b/src/tools/illink/src/linker/Linker/Driver.cs
@@ -618,6 +618,12 @@ protected int SetupContext (ILogger? customLogger = null)
continue;
}
+ case "--preserve-symbol-paths":
+ if (!GetBoolParam (token, l => context.PreserveSymbolPaths = l))
+ return -1;
+
+ continue;
+
case "--version":
Version ();
return 1;
@@ -1333,12 +1339,13 @@ static void Usage ()
Console.WriteLine ();
Console.WriteLine ("Options");
- Console.WriteLine (" -d PATH Specify additional directory to search in for assembly references");
- Console.WriteLine (" -reference FILE Specify additional file location used to resolve assembly references");
- Console.WriteLine (" -b Update debug symbols for all modified files. Defaults to false");
- Console.WriteLine (" -out PATH Specify the output directory. Defaults to 'output'");
- Console.WriteLine (" -h Lists all {0} options", _linker);
- Console.WriteLine (" @FILE Read response file for more options");
+ Console.WriteLine (" -d PATH Specify additional directory to search in for assembly references");
+ Console.WriteLine (" -reference FILE Specify additional file location used to resolve assembly references");
+ Console.WriteLine (" -b Update debug symbols for all modified files. Defaults to false");
+ Console.WriteLine (" --preserve-symbol-paths Preserve debug header paths to pdb files. Defaults to false");
+ Console.WriteLine (" -out PATH Specify the output directory. Defaults to 'output'");
+ Console.WriteLine (" -h Lists all {0} options", _linker);
+ Console.WriteLine (" @FILE Read response file for more options");
Console.WriteLine ();
Console.WriteLine ("Actions");
diff --git a/src/tools/illink/src/linker/Linker/LinkContext.cs b/src/tools/illink/src/linker/Linker/LinkContext.cs
index 9e6ec519d3054..ce8dd2b093661 100644
--- a/src/tools/illink/src/linker/Linker/LinkContext.cs
+++ b/src/tools/illink/src/linker/Linker/LinkContext.cs
@@ -108,6 +108,8 @@ public Pipeline Pipeline {
public bool LinkSymbols { get; set; }
+ public bool PreserveSymbolPaths { get; set; }
+
public bool KeepComInterfaces { get; set; }
public bool KeepMembersForDebugger { get; set; } = true;
diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.cs b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.cs
index ef05dde863289..14db921f5ae85 100644
--- a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.cs
+++ b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.cs
@@ -588,6 +588,27 @@ public void TestRemoveSymbolsDefault ()
}
}
+ [Theory]
+ [InlineData (true)]
+ [InlineData (false)]
+ public void TestPreserveSymbolPaths (bool preserveSymbolPaths)
+ {
+ var task = new MockTask () {
+ PreserveSymbolPaths = preserveSymbolPaths
+ };
+ using (var driver = task.CreateDriver ()) {
+ Assert.Equal (preserveSymbolPaths, driver.Context.PreserveSymbolPaths);
+ }
+ }
+
+ [Fact]
+ public void TestPreserveSymbolPathsDefault ()
+ {
+ var task = new MockTask ();
+ using (var driver = task.CreateDriver ()) {
+ Assert.False (driver.Context.PreserveSymbolPaths);
+ }
+ }
[Fact]
public void TestKeepCustomMetadata ()
diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs b/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs
index 979d62b1448ac..9a640c05b9f4c 100644
--- a/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs
+++ b/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs
@@ -51,6 +51,7 @@ public void SetOptimization (string optimization, bool enabled)
static readonly string[] nonOptimizationBooleanProperties = new string[] {
"DumpDependencies",
"RemoveSymbols",
+ "PreserveSymbolPaths",
"TreatWarningsAsErrors",
"SingleWarn"
};