Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
DependsOnTargets="CollectExtensionPackages;WriteExtensionProject;RestoreExtensionProject"
AfterTargets="Restore" />

<Target Name="PrepareFunctionsExtensionPayload" Condition="'$(DesignTimeBuild)' != 'true'"
DependsOnTargets="GetFunctionsExtensionFiles"
AfterTargets="CoreCompile" BeforeTargets="GetCopyToOutputDirectoryItems" />

<Target Name="PublishFunctionsExtensionPayload" Condition="'$(DesignTimeBuild)' != 'true'"
DependsOnTargets="GetFunctionsExtensionFiles"
BeforeTargets="GetCopyToPublishDirectoryItems" />

<Target Name="CollectExtensionPackages" Returns="@(_AzureFunctionPackageReference)">
<ResolveExtensionPackages ProjectAssetsFile="$(ProjectAssetsFile)">
<Output TaskParameter="ExtensionPackages" ItemName="_AzureFunctionPackageReference" />
Expand All @@ -46,4 +54,16 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
RemoveProperties="$(_AzureFunctionsExtensionRemoveProps)" Properties="IsRestoring=true;RestoreSources=$(_OutputSources)" />
</Target>

<Target Name="GetFunctionsExtensionFiles" Returns="@(_FunctionsExtensionsOutputItems)">
<MSBuild Projects="$(_AzureFunctionsExtensionProjectPath)" Targets="ResolveFunctionsExtensionFiles"
RemoveProperties="$(_AzureFunctionsExtensionRemoveProps)">
<Output TaskParameter="TargetOutputs" ItemName="_FunctionsExtensionsOutputItems" />
</MSBuild>

<ItemGroup>
<_NoneWithTargetPath Include="@(_FunctionsExtensionsOutputItems)"
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Target>

</Project>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
</PropertyGroup>

<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />
<Import Project="$(MSBuildThisFileDirectory)Azure.Functions.Sdk.Inner.RuntimePackages.props" />

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and

<Project>

<UsingTask TaskName="$(AzureFunctionsTaskNamespace).Inner.ResolveExtensionCopyLocal" AssemblyFile="$(AzureFunctionsSdkTasksAssembly)" />

<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />

<!--
Expand All @@ -21,4 +23,21 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
<Error Condition="'$(TargetFramework)' != 'net8.0'" Text="The target framework '$(TargetFramework)' must be 'net8.0'. Verify if target framework has been overridden by a global property." />
</Target>

<Target Name="ResolveFunctionsExtensionFiles"
DependsOnTargets="ResolveReferences;GenerateBuildDependencyFile;GetCopyToOutputDirectoryItems"
Returns="@(FunctionsExtensionFiles)">
<ResolveExtensionCopyLocal RuntimeAssemblies="@(_FunctionsRuntimeAssembly)" RuntimePackages="@(_FunctionsRuntimePackage)" CopyLocalFiles="@(ReferenceCopyLocalPaths)">
<Output TaskParameter="ExtensionsCopyLocal" ItemName="FunctionsExtensionFiles" />
</ResolveExtensionCopyLocal>

<ItemGroup>
<FunctionsExtensionFiles Include="@(AllItemsFullPathWithTargetPath)">
<TargetPath>$([System.IO.Path]::Combine(.azurefunctions, %(AllItemsFullPathWithTargetPath.TargetPath)))</TargetPath>
</FunctionsExtensionFiles>
<FunctionsExtensionFiles Include="$(ProjectDepsFilePath)">
<TargetPath>$([System.IO.Path]::Combine(.azurefunctions, function.deps.json))</TargetPath>
</FunctionsExtensionFiles>
</ItemGroup>
</Target>

</Project>
21 changes: 21 additions & 0 deletions src/Azure.Functions.Sdk/TaskItemExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Framework;

namespace Azure.Functions.Sdk;
Expand Down Expand Up @@ -39,5 +40,25 @@ public string SourcePackageId
get => taskItem.GetMetadata("SourcePackageId") ?? string.Empty;
set => taskItem.SetMetadata("SourcePackageId", value);
}

/// <summary>
/// Gets or sets the "NuGetPackageId" metadata on the task item.
/// </summary>
public string NuGetPackageId
{
get => taskItem.GetMetadata("NuGetPackageId") ?? string.Empty;
set => taskItem.SetMetadata("NuGetPackageId", value);
}

/// <summary>
/// Tries to get the NuGet package ID from the task item.
/// </summary>
/// <param name="packageId">The package ID, if found.</param>
/// <returns><c>true</c> if nuget package ID is found; <c>false</c> otherwise.</returns>
public bool TryGetNuGetPackageId([NotNullWhen(true)] out string? packageId)
{
packageId = taskItem.NuGetPackageId;
return !string.IsNullOrEmpty(packageId);
}
}
}
81 changes: 81 additions & 0 deletions src/Azure.Functions.Sdk/Tasks/Inner/ResolveExtensionCopyLocal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.Build.Framework;

namespace Azure.Functions.Sdk.Tasks.Inner;

/// <summary>
/// Resolves the set of extension assemblies that should be copied locally,
/// excluding those that are part of the Azure Functions runtime.
/// </summary>
public class ResolveExtensionCopyLocal : Microsoft.Build.Utilities.Task
{
/// <summary>
/// Gets or sets the runtime assemblies.
/// </summary>
/// <remarks>
/// These are the assemblies that are part of the Azure Functions runtime and should not be included
/// in the extensions payload.
/// </remarks>
[Required]
public ITaskItem[] RuntimeAssemblies { get; set; } = [];

/// <summary>
/// Gets or sets the runtime packages.
/// </summary>
/// <remarks>
/// These are packages that are part of the Azure Functions runtime and should not be included
/// in the extensions payload.
/// </remarks>
[Required]
public ITaskItem[] RuntimePackages { get; set; } = [];

/// <summary>
/// Gets or sets the copy local files.
/// </summary>
[Required]
public ITaskItem[] CopyLocalFiles { get; set; } = [];

/// <summary>
/// Gets the extensions copy local items.
/// </summary>
[Output]
public ITaskItem[] ExtensionsCopyLocal { get; private set; } = [];

public override bool Execute()
{
HashSet<string> runtimeAssemblies = new(
RuntimeAssemblies.Select(p => p.ItemSpec), StringComparer.OrdinalIgnoreCase);
HashSet<string> runtimePackages = new(
RuntimePackages.Select(p => p.ItemSpec), StringComparer.OrdinalIgnoreCase);

List<ITaskItem> extensionsCopyLocal = [];
foreach (ITaskItem item in CopyLocalFiles)
{
if (ShouldIncludeItem(item, runtimeAssemblies, runtimePackages))
{
string destination = item.GetMetadata("DestinationSubPath");
item.SetMetadata("TargetPath", Path.Combine(Constants.ExtensionsOutputFolder, destination));
extensionsCopyLocal.Add(item);
}
}

ExtensionsCopyLocal = [.. extensionsCopyLocal];
return !Log.HasLoggedErrors;
}

private static bool ShouldIncludeItem(
ITaskItem item, HashSet<string> runtimeAssemblies, HashSet<string> runtimePackages)
{
if (item.TryGetNuGetPackageId(out string? packageId) && runtimePackages.Contains(packageId))
{
// Comes from a runtime package, exclude.
return false;
}

// Check if the assembly name is in the runtime assemblies list.
string fileName = Path.GetFileName(item.ItemSpec);
return !runtimeAssemblies.Contains(fileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public AndConstraint<TaskItemAssertions> HaveMetadata(
return HaveMetadata(name, value, StringComparer.Ordinal, because, becauseArgs);
}

[CustomAssertion]
public AndConstraint<TaskItemAssertions> HaveMetadataLike(
string name, string value, string because = "", params object[] becauseArgs)
{
string actual = Subject.GetMetadata(name);
actual.Should().Match(value, because, becauseArgs);
return new AndConstraint<TaskItemAssertions>(this);
}

[CustomAssertion]
public AndConstraint<TaskItemAssertions> HaveMetadata(
string name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ protected virtual void Dispose(bool disposing)
}
}

protected string GetTempCsproj() => _temp.GetRandomFile(ext: ".csproj");
// Ensure this starts with a non-numeric character to be a valid csproj name.
protected string GetTempCsproj() => _temp.GetRandomCsproj();

private static string GetArtifactsPath()
{
Expand Down
107 changes: 105 additions & 2 deletions test/Azure.Functions.Sdk.Tests/Integration/SdkEndToEndTests.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ namespace Azure.Functions.Sdk.Tests.Integration;

public partial class SdkEndToEndTests
{
private static readonly string[] ExpectedExtensionFiles =
[
"Azure.Core.Amqp.dll",
"Azure.Core.dll",
"Azure.Identity.dll",
"Azure.Messaging.ServiceBus.dll",
"Azure.Storage.Blobs.dll",
"Azure.Storage.Common.dll",
"Azure.Storage.Queues.dll",
"function.deps.json",
"Google.Protobuf.dll",
"Grpc.AspNetCore.Server.ClientFactory.dll",
"Grpc.AspNetCore.Server.dll",
"Grpc.Core.Api.dll",
"Grpc.Net.Client.dll",
"Grpc.Net.ClientFactory.dll",
"Grpc.Net.Common.dll",
"Microsoft.Azure.Amqp.dll",
"Microsoft.Azure.WebJobs.Extensions.Rpc.dll",
"Microsoft.Azure.WebJobs.Extensions.ServiceBus.dll",
"Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.dll",
"Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll",
"Microsoft.Bcl.AsyncInterfaces.dll",
"Microsoft.Extensions.Azure.dll",
"Microsoft.Identity.Client.dll",
"Microsoft.Identity.Client.Extensions.Msal.dll",
"Microsoft.IdentityModel.Abstractions.dll",
"System.ClientModel.dll",
"System.IO.Hashing.dll",
];

[Fact]
public void Build_NetCore()
{
Expand All @@ -26,6 +57,7 @@ public void Build_NetCore()
Path.Combine(outputPath, "worker.config.json"),
"dotnet",
"MyFunctionApp.dll");
ValidateExtensionsPayload(outputPath, "function.deps.json");
}

[Fact]
Expand All @@ -48,6 +80,58 @@ public void Build_NetFx()
Path.Combine(outputPath, "worker.config.json"),
"{WorkerRoot}MyFunctionApp.exe",
"MyFunctionApp.exe");
ValidateExtensionsPayload(outputPath, "function.deps.json");
}

[Fact]
public void Build_NetCore_WithExtensions()
{
// Arrange
ProjectCreator project = ProjectCreator.Templates.AzureFunctionsProject(
GetTempCsproj(), targetFramework: "net8.0")
.Property("AssemblyName", "MyFunctionApp")
.WriteSourceFile("Program.cs", Resources.Program_Minimal_cs)
.ItemPackageReference(NugetPackage.ServiceBus)
.ItemPackageReference(NugetPackage.Storage);

// Act
BuildOutput output = project.Build(restore: true);

// Assert
output.Should().BeSuccessful().And.HaveNoIssues();
string outputPath = project.GetOutputPath();
ValidateConfig(
Path.Combine(outputPath, "worker.config.json"),
"dotnet",
"MyFunctionApp.dll");

ValidateExtensionsPayload(outputPath, ExpectedExtensionFiles);
}

[Fact]
public void Build_NetFx_WithExtensions()
{
// Arrange
ProjectCreator project = ProjectCreator.Templates.AzureFunctionsProject(
GetTempCsproj(), targetFramework: "net481")
.Property("AssemblyName", "MyFunctionApp")
.Property("LangVersion", "latest")
.WriteSourceFile("Program.cs", Resources.Program_Minimal_cs)
.ItemPackageReference(NugetPackage.ServiceBus)
.ItemPackageReference(NugetPackage.Storage);

// Act
BuildOutput output = project.Build(restore: true);

// Assert
output.Should().BeSuccessful().And.HaveNoIssues();
string outputPath = project.GetOutputPath();
ValidateConfig(
Path.Combine(outputPath, "worker.config.json"),
"{WorkerRoot}MyFunctionApp.exe",
"MyFunctionApp.exe");

ValidateExtensionsPayload(outputPath, ExpectedExtensionFiles);
}

[Fact]
Expand All @@ -68,16 +152,35 @@ public void Build_Incremental_NoOp()

string configPath = Path.Combine(outputPath, "worker.config.json");
ValidateConfig(configPath, "dotnet", "MyFunctionApp.dll");
ValidateExtensionsPayload(outputPath, "function.deps.json");

FileInfo config = new(configPath);
DateTime lastWriteTime = config.LastWriteTimeUtc;
DateTime configWriteTime = config.LastWriteTimeUtc;

FileInfo deps = new(Path.Combine(outputPath, "function.deps.json"));
DateTime depsWriteTime = deps.LastWriteTimeUtc;

// Act 2: Incremental build
BuildOutput output2 = project.Build();

// Assert 2: Verify no changes were made
output2.Should().BeSuccessful().And.HaveNoIssues();
config.Refresh();
config.LastWriteTimeUtc.Should().Be(lastWriteTime);
config.LastWriteTimeUtc.Should().Be(configWriteTime);

deps.Refresh();
deps.LastWriteTimeUtc.Should().Be(depsWriteTime);
}

private static void ValidateExtensionsPayload(string outputPath, params string[] expectedFiles)
{
string extensionsFolder = Path.Combine(outputPath, Constants.ExtensionsOutputFolder);
Directory.Exists(extensionsFolder).Should().BeTrue("Extensions folder should exist.");

HashSet<string> actualFiles = Directory.GetFiles(extensionsFolder, "*", SearchOption.AllDirectories)
.Select(f => Path.GetRelativePath(extensionsFolder, f))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

actualFiles.Should().BeEquivalentTo(expectedFiles);
}
}
Loading
Loading