Skip to content

Commit a8d43ce

Browse files
committed
Resolving issue with native dependencies collision
1 parent 28ba9d9 commit a8d43ce

File tree

5 files changed

+3654
-21
lines changed

5 files changed

+3654
-21
lines changed

src/WebJobs.Script/Description/DotNet/FunctionAssemblyLoadContext.cs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ public partial class FunctionAssemblyLoadContext : AssemblyLoadContext
2929
private static Lazy<FunctionAssemblyLoadContext> _defaultContext = new Lazy<FunctionAssemblyLoadContext>(CreateSharedContext, true);
3030

3131
private readonly List<string> _probingPaths = new List<string>();
32-
private readonly IDictionary<string, string> _depsAssemblies;
33-
private readonly IDictionary<string, string> _nativeLibraries;
32+
private readonly IDictionary<string, RuntimeAsset[]> _depsAssemblies;
33+
private readonly IDictionary<string, RuntimeAsset[]> _nativeLibraries;
34+
private readonly List<string> _currentRidFallback;
3435

3536
public FunctionAssemblyLoadContext(string basePath)
3637
{
@@ -39,7 +40,9 @@ public FunctionAssemblyLoadContext(string basePath)
3940
throw new ArgumentNullException(nameof(basePath));
4041
}
4142

42-
(_depsAssemblies, _nativeLibraries) = InitializeDeps(basePath);
43+
_currentRidFallback = DependencyHelper.GetRuntimeFallbacks();
44+
45+
(_depsAssemblies, _nativeLibraries) = InitializeDeps(basePath, _currentRidFallback);
4346

4447
_probingPaths.Add(basePath);
4548
}
@@ -51,26 +54,26 @@ internal static void ResetSharedContext()
5154
_defaultContext = new Lazy<FunctionAssemblyLoadContext>(CreateSharedContext, true);
5255
}
5356

54-
internal static (IDictionary<string, string> depsAssemblies, IDictionary<string, string> nativeLibraries) InitializeDeps(string basePath)
57+
internal static (IDictionary<string, RuntimeAsset[]> depsAssemblies, IDictionary<string, RuntimeAsset[]> nativeLibraries) InitializeDeps(string basePath, List<string> ridFallbacks)
5558
{
5659
string depsFilePath = Path.Combine(basePath, DotNetConstants.FunctionsDepsFileName);
5760

5861
if (File.Exists(depsFilePath))
5962
{
6063
try
6164
{
62-
List<string> rids = DependencyHelper.GetRuntimeFallbacks();
63-
6465
var reader = new DependencyContextJsonReader();
6566
using (Stream file = File.OpenRead(depsFilePath))
6667
{
6768
var depsContext = reader.Read(file);
68-
var depsAssemblies = depsContext.RuntimeLibraries.SelectMany(l => SelectRuntimeAssemblyGroup(rids, l.RuntimeAssemblyGroups))
69-
.ToDictionary(path => Path.GetFileNameWithoutExtension(path));
69+
var depsAssemblies = depsContext.RuntimeLibraries.SelectMany(l => SelectRuntimeAssemblyGroup(ridFallbacks, l.RuntimeAssemblyGroups))
70+
.GroupBy(a => Path.GetFileNameWithoutExtension(a.Path))
71+
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
7072

7173
// Note the difference here that nativeLibraries has the whole file name, including extension.
72-
var nativeLibraries = depsContext.RuntimeLibraries.SelectMany(l => SelectRuntimeAssemblyGroup(rids, l.NativeLibraryGroups))
73-
.ToDictionary(path => Path.GetFileName(path));
74+
var nativeLibraries = depsContext.RuntimeLibraries.SelectMany(l => SelectRuntimeAssemblyGroup(ridFallbacks, l.NativeLibraryGroups))
75+
.GroupBy(path => Path.GetFileName(path.Path))
76+
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
7477

7578
return (depsAssemblies, nativeLibraries);
7679
}
@@ -83,20 +86,20 @@ internal static (IDictionary<string, string> depsAssemblies, IDictionary<string,
8386
return (null, null);
8487
}
8588

86-
private static IEnumerable<string> SelectRuntimeAssemblyGroup(List<string> rids, IReadOnlyList<RuntimeAssetGroup> runtimeAssemblyGroups)
89+
private static IEnumerable<RuntimeAsset> SelectRuntimeAssemblyGroup(List<string> rids, IReadOnlyList<RuntimeAssetGroup> runtimeAssemblyGroups)
8790
{
8891
// Attempt to load group for the current RID graph
8992
foreach (var rid in rids)
9093
{
9194
var assemblyGroup = runtimeAssemblyGroups.FirstOrDefault(g => string.Equals(g.Runtime, rid, StringComparison.OrdinalIgnoreCase));
9295
if (assemblyGroup != null)
9396
{
94-
return assemblyGroup.AssetPaths;
97+
return assemblyGroup.AssetPaths.Select(path => new RuntimeAsset(rid, path));
9598
}
9699
}
97100

98101
// If unsuccessful, load default assets, making sure the path is flattened to reflect deployed files
99-
return runtimeAssemblyGroups.GetDefaultAssets().Select(a => Path.GetFileName(a));
102+
return runtimeAssemblyGroups.GetDefaultAssets().Select(a => new RuntimeAsset(null, Path.GetFileName(a)));
100103
}
101104

102105
private static FunctionAssemblyLoadContext CreateSharedContext()
@@ -224,7 +227,7 @@ private bool TryLoadDepsDependency(AssemblyName assemblyName, out Assembly assem
224227

225228
if (_depsAssemblies != null &&
226229
!IsRuntimeAssembly(assemblyName) &&
227-
_depsAssemblies.TryGetValue(assemblyName.Name, out string assemblyPath))
230+
TryGetDepsAsset(_depsAssemblies, assemblyName.Name, _currentRidFallback, out string assemblyPath))
228231
{
229232
foreach (var probingPath in _probingPaths)
230233
{
@@ -240,6 +243,42 @@ private bool TryLoadDepsDependency(AssemblyName assemblyName, out Assembly assem
240243
return assembly != null;
241244
}
242245

246+
internal static bool TryGetDepsAsset(IDictionary<string, RuntimeAsset[]> depsAssets, string assetName, List<string> ridFallbacks, out string assemblyPath)
247+
{
248+
assemblyPath = null;
249+
250+
if (depsAssets.TryGetValue(assetName, out RuntimeAsset[] assets))
251+
{
252+
// If we have a single asset match, return it:
253+
if (assets.Length == 1)
254+
{
255+
assemblyPath = assets[0].Path;
256+
}
257+
else
258+
{
259+
foreach (var rid in ridFallbacks)
260+
{
261+
RuntimeAsset match = assets.FirstOrDefault(a => string.Equals(rid, a.Rid, StringComparison.OrdinalIgnoreCase));
262+
263+
if (match != null)
264+
{
265+
assemblyPath = match.Path;
266+
break;
267+
}
268+
}
269+
270+
// If we're unable to locate a matching asset based on the RID fallback probing,
271+
// attempt to use a default/RID-agnostic asset instead
272+
if (assemblyPath == null)
273+
{
274+
assemblyPath = assets.FirstOrDefault(a => a.Rid == null)?.Path;
275+
}
276+
}
277+
}
278+
279+
return assemblyPath != null;
280+
}
281+
243282
private bool TryLoadRuntimeAssembly(AssemblyName assemblyName, out Assembly assembly)
244283
{
245284
assembly = null;
@@ -320,7 +359,7 @@ private string GetRuntimeNativeAssetPath(string assetFileName)
320359

321360
if (result == null && _nativeLibraries != null)
322361
{
323-
if (_nativeLibraries.TryGetValue(assetFileName, out string relativePath))
362+
if (TryGetDepsAsset(_nativeLibraries, assetFileName, _currentRidFallback, out string relativePath))
324363
{
325364
string nativeLibraryFullPath = Path.Combine(basePath, relativePath);
326365
if (File.Exists(nativeLibraryFullPath))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Diagnostics;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Description
7+
{
8+
[DebuggerDisplay("{" + nameof(Display) + ",nq}")]
9+
public class RuntimeAsset
10+
{
11+
public RuntimeAsset(string rid, string path)
12+
{
13+
Rid = rid;
14+
Path = path;
15+
}
16+
17+
public string Rid { get; }
18+
19+
public string Path { get; }
20+
21+
private string Display => $"({Rid ?? "no RID"}) - {Path}";
22+
}
23+
}

test/WebJobs.Script.Tests/Description/DotNet/FunctionAssemblyLoadContextTests.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using System.Runtime.Loader;
1111
using Microsoft.Azure.WebJobs.Script.Description;
1212
using Microsoft.Azure.WebJobs.Script.Extensibility;
13+
using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection;
14+
using Microsoft.Extensions.DependencyModel;
1315
using Moq;
1416
using Xunit;
1517

@@ -43,18 +45,23 @@ public void RuntimeAssemblies_AreLoadedInDefaultContext(string assemblyName)
4345
public void InitializeDeps_LoadsExpectedDependencies()
4446
{
4547
string depsPath = Path.Combine(Directory.GetCurrentDirectory(), "Description", "DotNet", "TestFiles", "DepsFiles");
48+
List<string> currentRidFallbacks = DependencyHelper.GetRuntimeFallbacks();
4649

47-
(IDictionary<string, string> depsAssemblies, IDictionary<string, string> nativeLibraries) =
48-
FunctionAssemblyLoadContext.InitializeDeps(depsPath);
50+
(IDictionary<string, RuntimeAsset[]> depsAssemblies, IDictionary<string, RuntimeAsset[]> nativeLibraries) =
51+
FunctionAssemblyLoadContext.InitializeDeps(depsPath, currentRidFallbacks);
4952

5053
string testRid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win" : "unix";
5154

5255
// Ensure runtime specific dependencies are resolved, with appropriate RID
53-
Assert.Contains($"runtimes/{testRid}/lib/netstandard2.0/System.Private.ServiceModel.dll", depsAssemblies.Values);
54-
Assert.Contains($"runtimes/{testRid}/lib/netstandard1.3/System.Text.Encoding.CodePages.dll", depsAssemblies.Values);
56+
FunctionAssemblyLoadContext.TryGetDepsAsset(depsAssemblies, "System.private.servicemodel", currentRidFallbacks, out string assemblyPath);
57+
Assert.Equal($"runtimes/{testRid}/lib/netstandard2.0/System.Private.ServiceModel.dll", assemblyPath);
58+
59+
FunctionAssemblyLoadContext.TryGetDepsAsset(depsAssemblies, "System.text.encoding.codepages", currentRidFallbacks, out assemblyPath);
60+
Assert.Equal($"runtimes/{testRid}/lib/netstandard1.3/System.Text.Encoding.CodePages.dll", assemblyPath);
5561

5662
// Ensure flattened dependency has expected path
57-
Assert.Contains($"Microsoft.Azure.WebJobs.Host.Storage.dll", depsAssemblies.Values);
63+
FunctionAssemblyLoadContext.TryGetDepsAsset(depsAssemblies, "Microsoft.Azure.WebJobs.Host.Storage", currentRidFallbacks, out assemblyPath);
64+
Assert.Equal($"Microsoft.Azure.WebJobs.Host.Storage.dll", assemblyPath);
5865

5966
// Ensure native libraries are resolved, with appropriate RID and path
6067
string nativeRid;
@@ -83,7 +90,36 @@ public void InitializeDeps_LoadsExpectedDependencies()
8390

8491
nativeRid += Environment.Is64BitProcess ? "x64" : "x86";
8592

86-
Assert.Contains($"runtimes/{nativeRid}/nativeassets/netstandard2.0/{nativePrefix}CpuMathNative.{nativeExtension}", nativeLibraries.Values);
93+
string nativeAssetFileName = $"{nativePrefix}CpuMathNative.{nativeExtension}";
94+
95+
FunctionAssemblyLoadContext.TryGetDepsAsset(nativeLibraries, nativeAssetFileName, currentRidFallbacks, out string assetPath);
96+
Assert.Contains($"runtimes/{nativeRid}/nativeassets/netstandard2.0/{nativeAssetFileName}", assetPath);
97+
}
98+
99+
[Theory]
100+
[InlineData("win10-x64", "win7-x64", "", "dll", true)]
101+
[InlineData("win7-x64", "win7-x64", "", "dll", true)]
102+
[InlineData("win8-x64", "win7-x64", "", "dll", true)]
103+
[InlineData("win10", "", "", "dll", false)]
104+
[InlineData("win-x64", "win-x64", "", "dll", true)]
105+
public void InitializeDeps_WithRidSpecificNativeAssets_LoadsExpectedDependencies(string rid, string expectedNativeRid, string prefix, string suffix, bool expectMatch)
106+
{
107+
string depsPath = Path.Combine(Directory.GetCurrentDirectory(), "Description", "DotNet", "TestFiles", "DepsFiles", "RidNativeDeps");
108+
109+
List<string> ridFallback = DependencyHelper.GetRuntimeFallbacks(rid);
110+
111+
(_, IDictionary<string, RuntimeAsset[]> nativeLibraries) =
112+
FunctionAssemblyLoadContext.InitializeDeps(depsPath, ridFallback);
113+
114+
string nativeAssetFileName = $"{prefix}Cosmos.CRTCompat.{suffix}";
115+
116+
FunctionAssemblyLoadContext.TryGetDepsAsset(nativeLibraries, nativeAssetFileName, ridFallback, out string assetPath);
117+
118+
string expectedMatch = expectMatch
119+
? $"runtimes/{expectedNativeRid}/native/{nativeAssetFileName}"
120+
: null;
121+
122+
Assert.Equal(expectedMatch, assetPath);
87123
}
88124
}
89125
}

0 commit comments

Comments
 (0)