Skip to content

Commit d1a30cc

Browse files
authored
Merge pull request #370 from MarcoRossignoli/fixnetstandardresolution
Try to fix netstandard.dll hell
2 parents cdbfff2 + a3c5d39 commit d1a30cc

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public InstrumenterResult Instrument()
8787
private void InstrumentModule()
8888
{
8989
using (var stream = new FileStream(_module, FileMode.Open, FileAccess.ReadWrite))
90-
using (var resolver = new DefaultAssemblyResolver())
90+
using (var resolver = new NetstandardAwareAssemblyResolver())
9191
{
9292
resolver.AddSearchDirectory(Path.GetDirectoryName(_module));
9393
var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver };
@@ -613,4 +613,94 @@ public MethodReference ImportReference(MethodReference method, IGenericParameter
613613
}
614614
}
615615
}
616+
617+
/// <summary>
618+
/// In case of testing different runtime i.e. netfx we could find netstandard.dll in folder.
619+
/// netstandard.dll is a forward only lib, there is no IL but only forwards to "runtime" implementation.
620+
/// For some classes implementation are in different assembly for different runtime for instance:
621+
///
622+
/// For NetFx 4.7
623+
/// // Token: 0x2700072C RID: 1836
624+
/// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
625+
/// {
626+
/// .assembly extern System
627+
/// }
628+
///
629+
/// For netcoreapp2.2
630+
/// Token: 0x2700072C RID: 1836
631+
/// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
632+
/// {
633+
/// .assembly extern System.Security.Cryptography.X509Certificates
634+
/// }
635+
///
636+
/// There is a concrete possibility that Cecil cannot find implementation and throws StackOverflow exception https://github.com/jbevain/cecil/issues/575
637+
/// This custom resolver check if requested lib is a "official" netstandard.dll and load once of "current runtime" with
638+
/// correct forwards.
639+
/// Check compares 'assembly name' and 'public key token', because versions could differ between runtimes.
640+
/// </summary>
641+
internal class NetstandardAwareAssemblyResolver : DefaultAssemblyResolver
642+
{
643+
private static System.Reflection.Assembly _netStandardAssembly;
644+
private static string _name;
645+
private static byte[] _publicKeyToken;
646+
private static AssemblyDefinition _assemblyDefinition;
647+
648+
static NetstandardAwareAssemblyResolver()
649+
{
650+
try
651+
{
652+
// To be sure to load information of "real" runtime netstandard implementation
653+
_netStandardAssembly = System.Reflection.Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"));
654+
System.Reflection.AssemblyName name = _netStandardAssembly.GetName();
655+
_name = name.Name;
656+
_publicKeyToken = name.GetPublicKeyToken();
657+
_assemblyDefinition = AssemblyDefinition.ReadAssembly(_netStandardAssembly.Location);
658+
}
659+
catch (FileNotFoundException)
660+
{
661+
// netstandard not supported
662+
}
663+
}
664+
665+
// Check name and public key but not version that could be different
666+
private bool CheckIfSearchingNetstandard(AssemblyNameReference name)
667+
{
668+
if (_netStandardAssembly is null)
669+
{
670+
return false;
671+
}
672+
673+
if (_name != name.Name)
674+
{
675+
return false;
676+
}
677+
678+
if (name.PublicKeyToken.Length != _publicKeyToken.Length)
679+
{
680+
return false;
681+
}
682+
683+
for (int i = 0; i < name.PublicKeyToken.Length; i++)
684+
{
685+
if (_publicKeyToken[i] != name.PublicKeyToken[i])
686+
{
687+
return false;
688+
}
689+
}
690+
691+
return true;
692+
}
693+
694+
public override AssemblyDefinition Resolve(AssemblyNameReference name)
695+
{
696+
if (CheckIfSearchingNetstandard(name))
697+
{
698+
return _assemblyDefinition;
699+
}
700+
else
701+
{
702+
return base.Resolve(name);
703+
}
704+
}
705+
}
616706
}

test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
using System;
22
using System.IO;
33
using System.Linq;
4-
using Xunit;
4+
using System.Reflection;
55

6+
using Coverlet.Core.Logging;
67
using Coverlet.Core.Samples.Tests;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.Emit;
11+
using Mono.Cecil;
712
using Moq;
8-
using Coverlet.Core.Logging;
13+
using Xunit;
14+
915

1016
namespace Coverlet.Core.Instrumentation.Tests
1117
{
@@ -141,5 +147,49 @@ class InstrumenterTest
141147

142148
public DirectoryInfo Directory { get; set; }
143149
}
150+
151+
[Fact]
152+
public void TestInstrument_NetStandardAwareAssemblyResolver_FromRuntime()
153+
{
154+
NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver();
155+
156+
// We ask for "official" netstandard.dll implementation with know MS public key cc7b13ffcd2ddd51 same in all runtime
157+
AssemblyDefinition resolved = netstandardResolver.Resolve(AssemblyNameReference.Parse("netstandard, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"));
158+
Assert.NotNull(resolved);
159+
160+
// We check that netstandard.dll was resolved from runtime folder, where System.Object is
161+
Assert.Equal(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"), resolved.MainModule.FileName);
162+
}
163+
164+
[Fact]
165+
public void TestInstrument_NetStandardAwareAssemblyResolver_FromFolder()
166+
{
167+
// Someone could create a custom dll named netstandard.dll we need to be sure that not
168+
// conflicts with "official" resolution
169+
170+
// We create dummy netstandard.dll
171+
CSharpCompilation compilation = CSharpCompilation.Create(
172+
"netstandard",
173+
new[] { CSharpSyntaxTree.ParseText("") },
174+
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) },
175+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
176+
177+
Assembly newAssemlby;
178+
using (var dllStream = new MemoryStream())
179+
{
180+
EmitResult emitResult = compilation.Emit(dllStream);
181+
Assert.True(emitResult.Success);
182+
newAssemlby = Assembly.Load(dllStream.ToArray());
183+
// remove if exists
184+
File.Delete("netstandard.dll");
185+
File.WriteAllBytes("netstandard.dll", dllStream.ToArray());
186+
}
187+
188+
NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver();
189+
AssemblyDefinition resolved = netstandardResolver.Resolve(AssemblyNameReference.Parse(newAssemlby.FullName));
190+
191+
// We check if final netstandard.dll resolved is local folder one and not "official" netstandard.dll
192+
Assert.Equal(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "netstandard.dll"), Path.GetFullPath(resolved.MainModule.FileName));
193+
}
144194
}
145195
}

test/coverlet.core.tests/coverlet.core.tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="$(MSBuildThisFileDirectory)\..\..\build\$(Configuration)\coverlet.msbuild.props" />
33

44
<PropertyGroup>
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.10.0" />
1011
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
1112
<PackageReference Include="Moq" Version="4.8.1" />
1213
<PackageReference Include="xunit" Version="2.3.1" />

0 commit comments

Comments
 (0)