Skip to content

Commit 078f29d

Browse files
committed
Automatic package restore
1 parent 8c372f1 commit 078f29d

File tree

22 files changed

+2373
-22
lines changed

22 files changed

+2373
-22
lines changed

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

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public sealed class DotNetFunctionInvoker : FunctionInvokerBase
4444

4545
internal DotNetFunctionInvoker(ScriptHost host, FunctionMetadata functionMetadata,
4646
Collection<FunctionBinding> inputBindings, Collection<FunctionBinding> outputBindings,
47-
IFunctionEntryPointResolver functionEntryPointResolver, FunctionAssemblyLoader assemblyLoader,
47+
IFunctionEntryPointResolver functionEntryPointResolver, FunctionAssemblyLoader assemblyLoader,
4848
ICompilationServiceFactory compilationServiceFactory, ITraceWriterFactory traceWriterFactory = null)
4949
: base(host, functionMetadata, traceWriterFactory)
5050
{
@@ -115,7 +115,7 @@ private void ReloadScript()
115115
// Reset cached function
116116
ResetFunctionValue();
117117
TraceOnPrimaryHost(string.Format(CultureInfo.InvariantCulture, "Script for function '{0}' changed. Reloading.", Metadata.Name), TraceLevel.Info);
118-
118+
119119
ImmutableArray<Diagnostic> compilationResult = ImmutableArray<Diagnostic>.Empty;
120120
FunctionSignature signature = null;
121121

@@ -169,22 +169,33 @@ private void ResetFunctionValue()
169169
}
170170

171171
private void RestorePackages()
172+
{
173+
// Kick off the package restore and return.
174+
// Any errors will be logged in RestorePackagesAsync
175+
RestorePackagesAsync(true)
176+
.ContinueWith(t => t.Exception.Handle(e => true), TaskContinuationOptions.OnlyOnFaulted);
177+
}
178+
179+
private async Task RestorePackagesAsync(bool reloadScriptOnSuccess = true)
172180
{
173181
TraceOnPrimaryHost("Restoring packages.", TraceLevel.Info);
174182

175-
_metadataResolver.RestorePackagesAsync()
176-
.ContinueWith(t =>
177-
{
178-
if (t.IsFaulted)
179-
{
180-
TraceOnPrimaryHost("Package restore failed:", TraceLevel.Info);
181-
TraceOnPrimaryHost(t.Exception.ToString(), TraceLevel.Info);
182-
return;
183-
}
183+
try
184+
{
185+
await _metadataResolver.RestorePackagesAsync();
186+
187+
TraceOnPrimaryHost("Packages restored.", TraceLevel.Info);
184188

185-
TraceOnPrimaryHost("Packages restored.", TraceLevel.Info);
189+
if (reloadScriptOnSuccess)
190+
{
186191
_reloadScript();
187-
});
192+
}
193+
}
194+
catch (Exception exc)
195+
{
196+
TraceOnPrimaryHost("Package restore failed:", TraceLevel.Error);
197+
TraceOnPrimaryHost(exc.ToString(), TraceLevel.Error);
198+
}
188199
}
189200

190201
public override async Task Invoke(object[] parameters)
@@ -298,10 +309,12 @@ internal async Task<MethodInfo> GetFunctionTargetAsync(int attemptCount = 0)
298309
return await GetFunctionTargetAsync(++attemptCount);
299310
}
300311

301-
private MethodInfo CreateFunctionTarget(CancellationToken cancellationToken)
312+
private async Task<MethodInfo> CreateFunctionTarget(CancellationToken cancellationToken)
302313
{
303314
try
304315
{
316+
await VerifyPackageReferencesAsync();
317+
305318
ICompilation compilation = _compilationService.GetFunctionCompilation(Metadata);
306319
FunctionSignature functionSignature = compilation.GetEntryPointSignature(_functionEntryPointResolver);
307320

@@ -326,6 +339,27 @@ private MethodInfo CreateFunctionTarget(CancellationToken cancellationToken)
326339
}
327340
}
328341

342+
private async Task VerifyPackageReferencesAsync()
343+
{
344+
try
345+
{
346+
if (_metadataResolver.RequiresPackageRestore(Metadata))
347+
{
348+
TraceOnPrimaryHost("Package references have been updated.", TraceLevel.Info);
349+
await RestorePackagesAsync(false);
350+
}
351+
}
352+
catch (Exception exc)
353+
{
354+
// There was an issue processing the package references,
355+
// wrap the exception in a CompilationErrorException and retrow
356+
TraceOnPrimaryHost("Error processing package references.", TraceLevel.Error);
357+
TraceOnPrimaryHost(exc.Message, TraceLevel.Error);
358+
359+
throw new CompilationErrorException("Unable to restore packages", ImmutableArray<Diagnostic>.Empty);
360+
}
361+
}
362+
329363
private void TraceCompilationDiagnostics(ImmutableArray<Diagnostic> diagnostics)
330364
{
331365
foreach (var diagnostic in diagnostics.Where(d => !d.IsSuppressed))

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,5 +258,10 @@ public async Task RestorePackagesAsync()
258258
// Reload the resolver
259259
_packageAssemblyResolver = new PackageAssemblyResolver(_functionMetadata);
260260
}
261+
262+
public bool RequiresPackageRestore(FunctionMetadata metadata)
263+
{
264+
return PackageManager.RequiresPackageRestore(Path.GetDirectoryName(metadata.ScriptFile));
265+
}
261266
}
262267
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ internal sealed class FunctionValueLoader : Lazy<Task<MethodInfo>>, IDisposable
1919
private readonly CancellationTokenSource _cts;
2020
private bool _disposed = false;
2121

22-
private FunctionValueLoader(Func<CancellationToken, MethodInfo> valueFactory, CancellationTokenSource cts)
23-
: base(() => Task.Factory.StartNew(() => valueFactory(cts.Token)), LazyThreadSafetyMode.ExecutionAndPublication)
22+
private FunctionValueLoader(Func<CancellationToken, Task<MethodInfo>> valueFactory, CancellationTokenSource cts)
23+
: base(() => valueFactory(cts.Token), LazyThreadSafetyMode.ExecutionAndPublication)
2424
{
2525
_cts = cts;
2626
_cts.Token.Register(CancellationRequested, false);
2727
}
2828

29-
public static FunctionValueLoader Create(Func<CancellationToken, MethodInfo> valueFactory)
29+
public static FunctionValueLoader Create(Func<CancellationToken, Task<MethodInfo>> valueFactory)
3030
{
3131
return new FunctionValueLoader(valueFactory, new CancellationTokenSource());
3232
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public interface IFunctionMetadataResolver
2424

2525
Task RestorePackagesAsync();
2626

27+
bool RequiresPackageRestore(FunctionMetadata metadata);
28+
2729
bool TryGetPackageReference(string referenceName, out PackageReference package);
2830
}
2931
}

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

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
57
using System.Diagnostics;
68
using System.Globalization;
79
using System.IO;
810
using System.Linq;
911
using System.Threading.Tasks;
1012
using Microsoft.Azure.WebJobs.Host;
13+
using Newtonsoft.Json.Linq;
14+
using NuGet.Frameworks;
15+
using NuGet.LibraryModel;
16+
using NuGet.Versioning;
1117

1218
namespace Microsoft.Azure.WebJobs.Script.Description
1319
{
@@ -98,6 +104,146 @@ public static string ResolveNuGetPath()
98104
return path ?? NuGetFileName;
99105
}
100106

107+
public static bool RequiresPackageRestore(string functionPath)
108+
{
109+
string projectFilePath = Path.Combine(functionPath, DotNetConstants.ProjectFileName);
110+
111+
if (!File.Exists(projectFilePath))
112+
{
113+
// If there's no project.json, we can just return from here
114+
// as there's nothing to restore
115+
return false;
116+
}
117+
118+
string lockFilePath = Path.Combine(functionPath, DotNetConstants.ProjectLockFileName);
119+
120+
if (!File.Exists(lockFilePath))
121+
{
122+
// If have a project.json and no lock file, we need to
123+
// restore the packages, just return true and skip validation
124+
return true;
125+
}
126+
127+
// This mimics the logic used by Nuget to validate a lock file against a given project.json file.
128+
// In order to determine whether we have a match, we:
129+
// - Read the project frameworks and their dependencies,
130+
// extracting the appropriate version range using the lock file format
131+
// - Read the lock file depenency groups
132+
// - Ensure that each project dependency matches a dependency in the lock file for the
133+
// appropriate group matching the framework (including non-framework specific/project wide dependencies)
134+
135+
var projectFrameworks = GetProjectDependencies(projectFilePath);
136+
var dependencyGroups = GetDependencyGroups(lockFilePath);
137+
138+
foreach (var dependencyGroup in dependencyGroups)
139+
{
140+
IOrderedEnumerable<string> projectDependencies;
141+
projectDependencies = projectFrameworks
142+
.FirstOrDefault(f => Equals(dependencyGroup.Framework, f.Framework))
143+
?.Dependencies?.OrderBy(d => d, StringComparer.OrdinalIgnoreCase);
144+
145+
if (!projectDependencies?.SequenceEqual(dependencyGroup.Dependencies.OrderBy(d => d, StringComparer.OrdinalIgnoreCase)) ?? false)
146+
{
147+
return true;
148+
}
149+
}
150+
151+
return false;
152+
}
153+
154+
private static IList<FrameworkInfo> GetProjectDependencies(string projectFilePath)
155+
{
156+
var frameworks = new List<FrameworkInfo>();
157+
158+
var jobject = JObject.Parse(File.ReadAllText(projectFilePath));
159+
var targetFrameworks = jobject.Value<JToken>("frameworks") as JObject;
160+
var projectDependenciesToken = jobject.Value<JToken>("dependencies") as JObject;
161+
162+
if (projectDependenciesToken != null)
163+
{
164+
IList<string> dependencies = ReadDependencies(projectDependenciesToken);
165+
frameworks.Add(new FrameworkInfo(null, dependencies));
166+
}
167+
168+
if (targetFrameworks != null)
169+
{
170+
foreach (var item in targetFrameworks)
171+
{
172+
var dependenciesToken = item.Value.SelectToken("dependencies") as JObject;
173+
174+
var framework = NuGetFramework.Parse(item.Key);
175+
IList<string> dependencies = ReadDependencies(dependenciesToken);
176+
177+
frameworks.Add(new FrameworkInfo(framework, dependencies));
178+
}
179+
}
180+
181+
return frameworks;
182+
}
183+
184+
private static IList<string> ReadDependencies(JObject dependenciesToken)
185+
{
186+
var dependencies = new List<string>();
187+
188+
if (dependenciesToken != null)
189+
{
190+
foreach (var dependency in dependenciesToken)
191+
{
192+
string name = dependency.Key;
193+
string version = null;
194+
195+
if (dependency.Value.Type == JTokenType.Object)
196+
{
197+
// { "PackageName" : { "version" :"1.0" ... }
198+
version = dependency.Value.Value<string>("version");
199+
}
200+
else if (dependency.Value.Type == JTokenType.String)
201+
{
202+
// { "PackageName" : "1.0" }
203+
version = dependency.Value.Value<string>();
204+
}
205+
else
206+
{
207+
throw new FormatException($"Unable to parse project.json file. Dependency '{name}' is not correctly formatted.");
208+
}
209+
210+
var libraryRange = new LibraryRange
211+
{
212+
Name = name,
213+
VersionRange = VersionRange.Parse(version)
214+
};
215+
216+
dependencies.Add(libraryRange.ToLockFileDependencyGroupString());
217+
}
218+
}
219+
220+
return dependencies;
221+
}
222+
223+
private static IList<FrameworkInfo> GetDependencyGroups(string projectLockFilePath)
224+
{
225+
var dependencyGroups = new List<FrameworkInfo>();
226+
227+
var jobject = JObject.Parse(File.ReadAllText(projectLockFilePath));
228+
var targetFrameworks = jobject.Value<JToken>("projectFileDependencyGroups") as JObject;
229+
230+
foreach (var dependencyGroup in targetFrameworks)
231+
{
232+
NuGetFramework framework = null;
233+
if (!string.IsNullOrEmpty(dependencyGroup.Key))
234+
{
235+
framework = NuGetFramework.Parse(dependencyGroup.Key);
236+
}
237+
238+
IList<string> dependencies = dependencyGroup.Value.ToObject<List<string>>();
239+
var frameworkInfo = new FrameworkInfo(framework, dependencies);
240+
241+
dependencyGroups.Add(frameworkInfo);
242+
}
243+
244+
return dependencyGroups;
245+
}
246+
101247
internal static string GetNugetPackagesPath()
102248
{
103249
string nugetHome = null;
@@ -122,5 +268,18 @@ private void ProcessDataReceived(object sender, DataReceivedEventArgs e)
122268
{
123269
_traceWriter.Info(e.Data ?? string.Empty);
124270
}
271+
272+
private class FrameworkInfo
273+
{
274+
public FrameworkInfo(NuGetFramework framework, IList<string> dependencies)
275+
{
276+
Framework = framework;
277+
Dependencies = new ReadOnlyCollection<string>(dependencies);
278+
}
279+
280+
public ReadOnlyCollection<string> Dependencies { get; }
281+
282+
public NuGetFramework Framework { get; }
283+
}
125284
}
126285
}

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#AddResponseHeader(System.Net.Http.HttpResponseMessage,System.Collections.Generic.KeyValuePair`2<System.String,Newtonsoft.Json.Linq.JToken>)")]
5252
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Active")]
5353
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Functions")]
54-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionValueLoader.#Create(System.Func`2<System.Threading.CancellationToken,System.Reflection.MethodInfo>)")]
5554
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FileTraceWriter.#.ctor(System.String,System.Diagnostics.TraceLevel)", Justification = "Disposed in IDisposable implementation.")]
5655
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.ScriptFunctionInvokerBase.#CreateTraceWriter(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,System.String)", Justification = "Disposed in IDisposable implementation.")]
5756
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#ApplyConfiguration(Newtonsoft.Json.Linq.JObject,Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration)")]
@@ -93,4 +92,4 @@
9392
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.DotNetFunctionDescriptorProvider.#GetFunctionParameters(Microsoft.Azure.WebJobs.Script.Description.IFunctionInvoker,Microsoft.Azure.WebJobs.Script.Description.FunctionMetadata,Microsoft.Azure.WebJobs.Script.Description.BindingMetadata,System.Collections.ObjectModel.Collection`1<System.Reflection.Emit.CustomAttributeBuilder>,System.Collections.ObjectModel.Collection`1<Microsoft.Azure.WebJobs.Script.Binding.FunctionBinding>,System.Collections.ObjectModel.Collection`1<Microsoft.Azure.WebJobs.Script.Binding.FunctionBinding>)")]
9493
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FunctionTraceWriterFactory.#Create()")]
9594
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#WatchDirectories")]
96-
95+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionValueLoader.#Create(System.Func`2<System.Threading.CancellationToken,System.Threading.Tasks.Task`1<System.Reflection.MethodInfo>>)")]

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@
174174
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
175175
<Private>True</Private>
176176
</Reference>
177+
<Reference Include="NuGet.Frameworks, Version=3.4.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
178+
<HintPath>..\..\packages\NuGet.Frameworks.3.4.3\lib\net45\NuGet.Frameworks.dll</HintPath>
179+
<Private>True</Private>
180+
</Reference>
181+
<Reference Include="NuGet.LibraryModel, Version=3.4.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
182+
<HintPath>..\..\packages\NuGet.LibraryModel.3.4.3\lib\net45\NuGet.LibraryModel.dll</HintPath>
183+
<Private>True</Private>
184+
</Reference>
185+
<Reference Include="NuGet.Versioning, Version=3.4.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
186+
<HintPath>..\..\packages\NuGet.Versioning.3.4.3\lib\net45\NuGet.Versioning.dll</HintPath>
187+
<Private>True</Private>
188+
</Reference>
177189
<Reference Include="RestSharp, Version=105.2.2.0, Culture=neutral, processorArchitecture=MSIL">
178190
<HintPath>..\..\packages\RestSharp.105.2.2\lib\net46\RestSharp.dll</HintPath>
179191
<Private>True</Private>

src/WebJobs.Script/packages.config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
<package id="Microsoft.WindowsAzure.ConfigurationManager" version="3.2.1" targetFramework="net46" />
3838
<package id="ncrontab" version="3.1.0" targetFramework="net46" />
3939
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net46" />
40+
<package id="NuGet.Frameworks" version="3.4.3" targetFramework="net46" />
41+
<package id="NuGet.LibraryModel" version="3.4.3" targetFramework="net46" />
42+
<package id="NuGet.Versioning" version="3.4.3" targetFramework="net46" />
4043
<package id="RestSharp" version="105.2.2" targetFramework="net46" />
4144
<package id="Sendgrid" version="6.3.4" targetFramework="net46" />
4245
<package id="SendGrid.SmtpApi" version="1.3.1" targetFramework="net45" />
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
using Moq;
1515
using Xunit;
1616

17-
namespace Microsoft.Azure.WebJobs.Script.Tests.Description.CSharp
17+
namespace Microsoft.Azure.WebJobs.Script.Tests.Description.DotNet
1818
{
1919
public class FunctionAssemblyLoaderTests
2020
{
File renamed without changes.

0 commit comments

Comments
 (0)