Skip to content

Commit 0080db7

Browse files
committed
Fixing a few issues that were primarily impacting F#:
1 - Ensuring that package resolver does not return duplicate references 2 - Ensuring that the MetadataResolver handles duplicates when adding default references 3 - Fixing F# compilation service to get the script references and removing dependency on C# compilation 4 - Fixing issue preventing the load context from being initialized for F#, which would trigger dependency resolution failures.
1 parent 13de74f commit 0080db7

File tree

8 files changed

+53
-80
lines changed

8 files changed

+53
-80
lines changed

src/WebJobs.Script/Description/DotNet/CSharp/CSharpCompilationService.cs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,23 @@ namespace Microsoft.Azure.WebJobs.Script.Description
1818
[CLSCompliant(false)]
1919
public class CSharpCompilationService : ICompilationService
2020
{
21-
private readonly IFunctionMetadataResolver _metadataResolver;
21+
private static readonly string[] FileTypes = { ".csx", ".cs" };
2222
private static readonly Encoding UTF8WithNoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
2323
private static readonly Lazy<InteractiveAssemblyLoader> AssemblyLoader
2424
= new Lazy<InteractiveAssemblyLoader>(() => new InteractiveAssemblyLoader(), LazyThreadSafetyMode.ExecutionAndPublication);
2525

2626
private readonly OptimizationLevel _optimizationLevel;
27+
private readonly IFunctionMetadataResolver _metadataResolver;
2728

2829
public CSharpCompilationService(IFunctionMetadataResolver metadataResolver, OptimizationLevel optimizationLevel)
2930
{
3031
_metadataResolver = metadataResolver;
3132
_optimizationLevel = optimizationLevel;
3233
}
3334

34-
public string Language
35-
{
36-
get
37-
{
38-
return "CSharp";
39-
}
40-
}
35+
public string Language => "CSharp";
4136

42-
public IEnumerable<string> SupportedFileTypes
43-
{
44-
get
45-
{
46-
return new[] { ".csx", ".cs" };
47-
}
48-
}
37+
public IEnumerable<string> SupportedFileTypes => FileTypes;
4938

5039
public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
5140
{

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Immutable;
66
using System.Globalization;
7+
using Microsoft.Azure.WebJobs.Host;
78
using Microsoft.Azure.WebJobs.Script.Config;
89
using Microsoft.CodeAnalysis;
910

@@ -14,15 +15,15 @@ public sealed class DotNetCompilationServiceFactory : ICompilationServiceFactory
1415
{
1516
private static readonly ImmutableArray<ScriptType> SupportedScriptTypes = new[] { ScriptType.CSharp, ScriptType.FSharp, ScriptType.DotNetAssembly }.ToImmutableArray();
1617
private static OptimizationLevel? _optimizationLevel;
18+
private readonly TraceWriter _traceWriter;
1719

18-
ImmutableArray<ScriptType> ICompilationServiceFactory.SupportedScriptTypes
20+
public DotNetCompilationServiceFactory(TraceWriter traceWriter)
1921
{
20-
get
21-
{
22-
return SupportedScriptTypes;
23-
}
22+
_traceWriter = traceWriter;
2423
}
2524

25+
ImmutableArray<ScriptType> ICompilationServiceFactory.SupportedScriptTypes => SupportedScriptTypes;
26+
2627
internal static OptimizationLevel OptimizationLevel
2728
{
2829
get
@@ -60,7 +61,7 @@ public ICompilationService CreateService(ScriptType scriptType, IFunctionMetadat
6061
case ScriptType.CSharp:
6162
return new CSharpCompilationService(metadataResolver, OptimizationLevel);
6263
case ScriptType.FSharp:
63-
return new FSharpCompiler(metadataResolver, OptimizationLevel);
64+
return new FSharpCompiler(metadataResolver, OptimizationLevel, _traceWriter);
6465
case ScriptType.DotNetAssembly:
6566
return new RawAssemblyCompilationService();
6667
default:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal sealed class DotNetFunctionDescriptorProvider : FunctionDescriptorProvi
2121
private readonly ICompilationServiceFactory _compilationServiceFactory;
2222

2323
public DotNetFunctionDescriptorProvider(ScriptHost host, ScriptHostConfiguration config)
24-
: this(host, config, new DotNetCompilationServiceFactory())
24+
: this(host, config, new DotNetCompilationServiceFactory(host.TraceWriter))
2525
{
2626
}
2727

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,16 @@ private async Task<MethodInfo> CreateFunctionTarget(CancellationToken cancellati
258258
using (_metricsLogger.LatencyEvent(eventName))
259259
{
260260
ICompilation compilation = _compilationService.GetFunctionCompilation(Metadata);
261+
262+
Assembly assembly = compilation.EmitAndLoad(cancellationToken);
263+
_assemblyLoader.CreateOrUpdateContext(Metadata, assembly, _metadataResolver, TraceWriter);
264+
261265
FunctionSignature functionSignature = compilation.GetEntryPointSignature(_functionEntryPointResolver);
262266

263267
ImmutableArray<Diagnostic> bindingDiagnostics = ValidateFunctionBindingArguments(functionSignature, _triggerInputName, _inputBindings, _outputBindings, throwIfFailed: true);
264268
TraceCompilationDiagnostics(bindingDiagnostics);
265269

266-
Assembly assembly = compilation.EmitAndLoad(cancellationToken);
267-
_assemblyLoader.CreateOrUpdateContext(Metadata, assembly, _metadataResolver, TraceWriter);
268-
269-
// Get our function entry point
270+
// Set our function entry point signature
270271
_functionSignature = functionSignature;
271272
System.Reflection.TypeInfo scriptType = assembly.DefinedTypes
272273
.FirstOrDefault(t => string.Compare(t.Name, functionSignature.ParentTypeName, StringComparison.Ordinal) == 0);

src/WebJobs.Script/Description/DotNet/FSharp/FSharpCompilationService.cs

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO;
7+
using System.Linq;
78
using System.Reflection;
89
using System.Text;
910
using System.Text.RegularExpressions;
1011
using System.Threading;
1112
using System.Threading.Tasks;
13+
using Microsoft.Azure.WebJobs.Host;
1214
using Microsoft.CodeAnalysis;
1315
using Microsoft.CodeAnalysis.Scripting;
1416
using Microsoft.CodeAnalysis.Scripting.Hosting;
@@ -22,36 +24,26 @@ namespace Microsoft.Azure.WebJobs.Script.Description
2224
{
2325
internal class FSharpCompiler : ICompilationService
2426
{
25-
private static readonly string[] TheWatchedFileTypes = { ".fs", ".fsx", ".dll", ".exe", ".fsi" };
27+
private static readonly string[] FileTypes = { ".fs", ".fsx", ".dll", ".exe", ".fsi" };
2628
private readonly IFunctionMetadataResolver _metadataResolver;
2729
private static readonly Lazy<InteractiveAssemblyLoader> AssemblyLoader
2830
= new Lazy<InteractiveAssemblyLoader>(() => new InteractiveAssemblyLoader(), LazyThreadSafetyMode.ExecutionAndPublication);
2931

3032
private readonly OptimizationLevel _optimizationLevel;
3133
private readonly Regex _hashRRegex;
34+
private readonly TraceWriter _traceWriter;
3235

33-
public FSharpCompiler(IFunctionMetadataResolver metadataResolver, OptimizationLevel optimizationLevel)
36+
public FSharpCompiler(IFunctionMetadataResolver metadataResolver, OptimizationLevel optimizationLevel, TraceWriter traceWriter)
3437
{
3538
_metadataResolver = metadataResolver;
3639
_optimizationLevel = optimizationLevel;
40+
_traceWriter = traceWriter;
3741
_hashRRegex = new Regex(@"^\s*#r\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
3842
}
3943

40-
public string Language
41-
{
42-
get
43-
{
44-
return "FSharp";
45-
}
46-
}
44+
public string Language => "FSharp";
4745

48-
public IEnumerable<string> SupportedFileTypes
49-
{
50-
get
51-
{
52-
return TheWatchedFileTypes;
53-
}
54-
}
46+
public IEnumerable<string> SupportedFileTypes => FileTypes;
5547

5648
public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
5749
{
@@ -77,7 +69,6 @@ public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
7769
var resolverSource = resolverSourceBuilder.ToString();
7870

7971
Script<object> script = CodeAnalysis.CSharp.Scripting.CSharpScript.Create(resolverSource, options: _metadataResolver.CreateScriptOptions(), assemblyLoader: AssemblyLoader.Value);
80-
Compilation compilation = script.GetCompilation();
8172

8273
var compiler = new SimpleSourceCodeServices(msbuildEnabled: FSharpOption<bool>.Some(false));
8374

@@ -90,7 +81,7 @@ public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
9081

9182
string scriptFilePath = Path.Combine(scriptPath, Path.GetFileName(functionMetadata.ScriptFile));
9283

93-
var assemblyName = FunctionAssemblyLoader.GetAssemblyNameFromMetadata(functionMetadata, compilation.AssemblyName);
84+
var assemblyName = FunctionAssemblyLoader.GetAssemblyNameFromMetadata(functionMetadata, Guid.NewGuid().ToString());
9485
var assemblyFileName = Path.Combine(scriptPath, assemblyName + ".dll");
9586
var pdbName = Path.ChangeExtension(assemblyFileName, "pdb");
9687

@@ -123,31 +114,16 @@ public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
123114
// as dictated by the C# reference resolver, and F# doesn't like getting multiple references to those.
124115
otherFlags.Add("--noframework");
125116

126-
// Add the references as reported by the C# reference resolver.
127-
foreach (var mdr in compilation.References)
128-
{
129-
if (!mdr.Display.Contains("Unresolved "))
117+
var references = script.Options.MetadataReferences
118+
.Cast<UnresolvedMetadataReference>()
119+
.Aggregate(new List<PortableExecutableReference>(), (a, r) =>
130120
{
131-
otherFlags.Add("-r:" + mdr.Display);
132-
}
133-
}
121+
a.AddRange(_metadataResolver.ResolveReference(r.Reference, string.Empty, r.Properties));
122+
return a;
123+
});
134124

135-
// Above we have used the C# reference resolver to get the basic set of DLL references for the compilation.
136-
//
137-
// However F# has its own view on default options. For scripts these should include the
138-
// following framework facade references.
139-
140-
otherFlags.Add("-r:System.Linq.dll"); // System.Linq.Expressions.Expression<T>
141-
otherFlags.Add("-r:System.Reflection.dll"); // System.Reflection.ParameterInfo
142-
otherFlags.Add("-r:System.Linq.Expressions.dll"); // System.Linq.IQueryable<T>
143-
otherFlags.Add("-r:System.Threading.Tasks.dll"); // valuetype [System.Threading.Tasks]System.Threading.CancellationToken
144-
otherFlags.Add("-r:System.IO.dll"); // System.IO.TextWriter
145-
otherFlags.Add("-r:System.Net.Requests.dll"); // System.Net.WebResponse etc.
146-
otherFlags.Add("-r:System.Collections.dll"); // System.Collections.Generic.List<T>
147-
otherFlags.Add("-r:System.Runtime.Numerics.dll"); // BigInteger
148-
otherFlags.Add("-r:System.Threading.dll"); // OperationCanceledException
149-
otherFlags.Add("-r:System.Runtime.dll");
150-
otherFlags.Add("-r:System.Numerics.dll");
125+
// Add the references as reported by the metadata resolver.
126+
otherFlags.AddRange(references.Select(r => "-r:" + r.Display));
151127

152128
if (_optimizationLevel == OptimizationLevel.Debug)
153129
{
@@ -196,13 +172,17 @@ public ICompilation GetFunctionCompilation(FunctionMetadata functionMetadata)
196172
var assembly = Assembly.Load(assemblyBytes, pdbBytes);
197173
assemblyOption = FSharpOption<Assembly>.Some(assembly);
198174
}
175+
else
176+
{
177+
_traceWriter.Verbose($"F# compilation failed with arguments: { string.Join(" ", otherFlags) }");
178+
}
199179
}
200180
finally
201181
{
202182
DeleteDirectoryAsync(scriptPath, recursive: true)
203183
.ContinueWith(t => t.Exception.Handle(e =>
204184
{
205-
// TODO: Trace
185+
_traceWriter.Warning($"Unable to delete F# compilation file: { e.ToString() }");
206186
return true;
207187
}), TaskContinuationOptions.OnlyOnFaulted);
208188
}

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ public sealed class FunctionMetadataResolver : MetadataReferenceResolver, IFunct
4343
"System.Configuration",
4444
"System.Xml",
4545
"System.Net.Http",
46+
"System.Runtime",
47+
"System.Threading.Tasks",
4648
"Microsoft.CSharp",
47-
typeof(object).Assembly.Location,
4849
typeof(IAsyncCollector<>).Assembly.Location, /*Microsoft.Azure.WebJobs*/
4950
typeof(JobHost).Assembly.Location, /*Microsoft.Azure.WebJobs.Host*/
5051
typeof(CoreJobHostConfigurationExtensions).Assembly.Location, /*Microsoft.Azure.WebJobs.Extensions*/
@@ -117,13 +118,11 @@ public override int GetHashCode()
117118

118119
public IReadOnlyCollection<string> GetCompilationReferences()
119120
{
120-
// Add package reference assemblies
121-
var result = new List<string>(_packageAssemblyResolver.AssemblyReferences);
122-
123-
// Add default references
124-
result.AddRange(DefaultAssemblyReferences);
121+
// Combine our default references with package references
122+
var combinedReferences = DefaultAssemblyReferences
123+
.Union(_packageAssemblyResolver.AssemblyReferences);
125124

126-
return result.AsReadOnly();
125+
return new ReadOnlyCollection<string>(combinedReferences.ToList());
127126
}
128127

129128
/// <summary>
@@ -189,7 +188,7 @@ private ImmutableArray<PortableExecutableReference> GetMetadataFromReferencePath
189188
{
190189
bool externalReference = false;
191190
string basePath = _privateAssembliesPath;
192-
191+
193192
// If this is a relative assembly reference, use the function directory as the base probing path
194193
if (reference.IndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) > -1)
195194
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public IEnumerable<string> AssemblyReferences
4444
assemblies.AddRange(p.Assemblies.Values);
4545
assemblies.AddRange(p.FrameworkAssemblies.Values);
4646
return assemblies;
47-
});
47+
}).Distinct();
4848
}
4949
}
5050

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Diagnostics;
88
using System.IO;
99
using System.Linq;
10+
using System.Threading;
1011
using System.Threading.Tasks;
1112
using Microsoft.Azure.WebJobs.Script.Binding;
1213
using Microsoft.Azure.WebJobs.Script.Description;
@@ -33,7 +34,9 @@ public async Task ReloadScript_WithInvalidCompilationAndMissingMethod_ReportsRes
3334
// Create the invoker dependencies and setup the appropriate method to throw the exception
3435
RunDependencies dependencies = CreateDependencies();
3536
dependencies.Compilation.Setup(c => c.GetEntryPointSignature(It.IsAny<IFunctionEntryPointResolver>()))
36-
.Throws(exception);
37+
.Throws(exception);
38+
dependencies.Compilation.Setup(c => c.EmitAndLoad(It.IsAny<CancellationToken>()))
39+
.Returns(typeof(object).Assembly);
3740

3841
string rootFunctionsFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
3942
Directory.CreateDirectory(rootFunctionsFolder);
@@ -102,7 +105,7 @@ public async Task Compilation_WithMissingBindingArguments_LogsAF004Warning()
102105

103106
var invoker = new DotNetFunctionInvoker(dependencies.Host.Object, metadata, new Collection<FunctionBinding>(),
104107
new Collection<FunctionBinding> { testBinding.Object }, new FunctionEntryPointResolver(), new FunctionAssemblyLoader(string.Empty),
105-
new DotNetCompilationServiceFactory(), dependencies.TraceWriterFactory.Object);
108+
new DotNetCompilationServiceFactory(NullTraceWriter.Instance), dependencies.TraceWriterFactory.Object);
106109

107110
await invoker.GetFunctionTargetAsync();
108111

@@ -140,7 +143,7 @@ public async Task Compilation_OnSecondaryHost_SuppressesLogs()
140143

141144
var invoker = new DotNetFunctionInvoker(dependencies.Host.Object, metadata, new Collection<FunctionBinding>(),
142145
new Collection<FunctionBinding> { testBinding.Object }, new FunctionEntryPointResolver(), new FunctionAssemblyLoader(string.Empty),
143-
new DotNetCompilationServiceFactory(), dependencies.TraceWriterFactory.Object);
146+
new DotNetCompilationServiceFactory(NullTraceWriter.Instance), dependencies.TraceWriterFactory.Object);
144147

145148
await invoker.GetFunctionTargetAsync();
146149

0 commit comments

Comments
 (0)