Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,34 @@ Tasks, and targets to support workload testing in `dotnet` repositories.
- `$(InstallWorkloadForTesting)` - required
- `$(BuiltNuGetsDir)` - required
- `$(DotNetInstallArgumentsForWorkloadsTesting)` - required
- `$(TemplateNuGetConfigPathForWorkloadTesting)` - required

- `$(TestUsingWorkloads)` - optional
- `$(SkipTempDirectoryCleanup)` - optional
- `$(VersionBandForManifestPackages)` - optional
- `$(ExtraWorkloadInstallCommandArguments)` - optional
- `$(WorkloadInstallCommandOutputImportance)` - optional, defaults to `Normal`

## `$(PackageSourceNameForBuiltPackages)` - optional

`<add key="<$sourceName>" value="file:///..." />`

Defaults to `nuget-local`.

## `$(NuGetConfigPackageSourceMappingsForWorkloadTesting)` - optional

For a value of `*Aspire*;Foo*`, a package source mapping will be added to the local nuget source
added for built nugets:

```xml
<packageSourceMapping>
<packageSource key="nuget-local">
<package pattern="*Aspire*" />
<package pattern="Foo*" />
</packageSource>
...
```

## items
# items

- `@(DefaultPropertiesForNuGetBuild)`
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@
TaskName="Microsoft.Workload.Build.Tasks.InstallWorkloadFromArtifacts"
AssemblyFile="$(WorkloadBuildTasksAssemblyPath)" />

<UsingTask Condition="'$(WorkloadBuildTasksAssemblyPath)' != ''"
TaskName="Microsoft.Workload.Build.Tasks.PatchNuGetConfig"
AssemblyFile="$(WorkloadBuildTasksAssemblyPath)" />

<Target Name="InstallWorkloadUsingArtifacts"
AfterTargets="$(InstallWorkloadUsingArtifactsAfterThisTarget)"
DependsOnTargets="$(InstallWorkloadUsingArtifactsDependsOn)"
Expand Down Expand Up @@ -201,8 +205,11 @@
VersionBandForManifestPackages="$(VersionBandForManifestPackages)"
LocalNuGetsPath="$(BuiltNugetsDir)"
ExtraWorkloadInstallCommandArguments="$(ExtraWorkloadInstallCommandArguments)"
PackageSourceNameForBuiltPackages="$(PackageSourceNameForBuiltPackages)"
TemplateNuGetConfigPath="$(TemplateNuGetConfigPathForWorkloadTesting)"
NuGetConfigPackageSourceMappings="$(NuGetConfigPackageSourceMappingsForWorkloadTesting)"
SdkWithNoWorkloadInstalledPath="$(_SdkWithNoWorkloadPath)"
WorkloadInstallCommandOutputImportance="$(WorkloadInstallCommandOutputImportance)"
IntermediateOutputPath="$(ArtifactsObjDir)"
SkipTempDirectoryCleanup="$(SkipTempDirectoryCleanup)"
/>
Expand Down
37 changes: 23 additions & 14 deletions src/tasks/WorkloadBuildTasks/InstallWorkloadFromArtifacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

#nullable enable

namespace Microsoft.Workload.Build.Tasks
{
public partial class InstallWorkloadFromArtifacts : Task
public partial class InstallWorkloadFromArtifacts : PatchNuGetConfig
{
[Required, NotNull]
public ITaskItem[] WorkloadIds { get; set; } = Array.Empty<ITaskItem>();
Expand All @@ -33,12 +36,6 @@ public partial class InstallWorkloadFromArtifacts : Task
[Required, NotNull]
public string? VersionBandForManifestPackages { get; set; }

[Required, NotNull]
public string? LocalNuGetsPath { get; set; }

[Required, NotNull]
public string? TemplateNuGetConfigPath { get; set; }

[Required, NotNull]
public string SdkWithNoWorkloadInstalledPath { get; set; } = string.Empty;

Expand All @@ -47,7 +44,9 @@ public partial class InstallWorkloadFromArtifacts : Task
public bool OnlyUpdateManifests { get; set; }
public bool SkipTempDirectoryCleanup { get; set; }

private const string s_nugetInsertionTag = "<!-- TEST_RESTORE_SOURCES_INSERTION_LINE -->";
// Should match enum values for MessageImportance - Low, Normal (default), High
public string? WorkloadInstallCommandOutputImportance { get; set; }

private string AllManifestsStampPath => Path.Combine(SdkWithNoWorkloadInstalledPath, ".all-manifests.stamp");
private string _tempDir = string.Empty;
private string _nugetCachePath = string.Empty;
Expand All @@ -67,6 +66,10 @@ public override bool Execute()
Directory.Delete(_tempDir, recursive: true);
Directory.CreateDirectory(_tempDir);
_nugetCachePath = Path.Combine(_tempDir, "nuget-cache");
if (SkipTempDirectoryCleanup)
{
Log.LogMessage(MessageImportance.High, $"Using temporary directory {_tempDir} for installing workloads from artifacts.");
}

try
{
Expand Down Expand Up @@ -217,6 +220,12 @@ private bool InstallPacks(InstallWorkloadRequest req, string nugetConfigContents
string nugetConfigPath = Path.Combine(_tempDir, $"NuGet.{Path.GetRandomFileName()}.config");
File.WriteAllText(nugetConfigPath, nugetConfigContents);

if (string.IsNullOrEmpty(WorkloadInstallCommandOutputImportance) ||
!Enum.TryParse<MessageImportance>(WorkloadInstallCommandOutputImportance, out var outputImportance))
{
outputImportance = MessageImportance.Normal;
}

// Log.LogMessage(MessageImportance.High, $"{Environment.NewLine}** dotnet workload install {req.WorkloadId} **{Environment.NewLine}");
(int exitCode, string output) = Utils.TryRunProcess(
Log,
Expand All @@ -228,7 +237,7 @@ private bool InstallPacks(InstallWorkloadRequest req, string nugetConfigContents
},
logStdErrAsMessage: req.IgnoreErrors,
silent: false,
debugMessageImportance: MessageImportance.Normal);
debugMessageImportance: outputImportance);
if (exitCode != 0)
{
if (req.IgnoreErrors)
Expand All @@ -255,11 +264,11 @@ private bool InstallPacks(InstallWorkloadRequest req, string nugetConfigContents

private string GetNuGetConfig()
{
string contents = File.ReadAllText(TemplateNuGetConfigPath);
if (!contents.Contains(s_nugetInsertionTag, StringComparison.InvariantCultureIgnoreCase))
throw new LogAsErrorException($"Could not find {s_nugetInsertionTag} in {TemplateNuGetConfigPath}");

return contents.Replace(s_nugetInsertionTag, $@"<add key=""nuget-local"" value=""file://{LocalNuGetsPath}"" />");
var nugetConfigPath = Path.GetTempFileName();
PatchNuGetConfig.GetNuGetConfig(TemplateNuGetConfigPath, LocalNuGetsPath, PackageSourceNameForBuiltPackages, NuGetConfigPackageSourceMappings, nugetConfigPath);
string contents = File.ReadAllText(nugetConfigPath);
File.Delete(nugetConfigPath);
return contents;
}

private bool InstallWorkloadManifest(ITaskItem workloadId, string name, string version, string sdkDir, string nugetConfigContents, bool stopOnMissing)
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/WorkloadBuildTasks/PackageInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private static string GenerateProject(IEnumerable<PackageReference> references)
<ItemGroup>");

foreach (var reference in references)
projectFileBuilder.AppendLine($"<PackageReference Include=\"{reference.Name}\" Version=\"{reference.Version}\" />");
projectFileBuilder.AppendLine($"<PackageDownload Include=\"{reference.Name}\" Version=\"[{reference.Version}]\" />");

projectFileBuilder.Append(@"
</ItemGroup>
Expand Down
140 changes: 140 additions & 0 deletions src/tasks/WorkloadBuildTasks/PatchNuGetConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

#nullable enable

namespace Microsoft.Workload.Build.Tasks;

/*
* Used for patching a nuget.config to:
*
* 1. Add a new package source to the nuget.config
* 2. Add a new package source mapping to the nuget.config
*
* This is useful specifically the case of workload testing
*/
public class PatchNuGetConfig : Task
{
[Required, NotNull]
public string? TemplateNuGetConfigPath { get; set; }

[Required, NotNull]
public string? LocalNuGetsPath { get; set; }

public string? OutputPath { get; set; }

/*
* Value: ["*Aspire*", "Foo*"]
* This will be translated to:
* <packageSourceMapping>
* <packageSource key="nuget-local">
* <package pattern="*Aspire*" />
* <package pattern="Foo*" />
* </packageSource>
*
* This is useful when using Central Package Management (https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management)
*/
public string[] NuGetConfigPackageSourceMappings { get; set; } = Array.Empty<string>();

public string PackageSourceNameForBuiltPackages { get; set; } = "nuget-local";

public override bool Execute()
{
try
{
Validate(TemplateNuGetConfigPath, PackageSourceNameForBuiltPackages, OutputPath);
GetNuGetConfig(TemplateNuGetConfigPath, LocalNuGetsPath, PackageSourceNameForBuiltPackages, NuGetConfigPackageSourceMappings, OutputPath!);
Log.LogMessage(MessageImportance.Low, $"Generated patched nuget.config at {OutputPath}");
return true;
}
catch (LogAsErrorException laee)
{
Log.LogError(laee.Message);
return false;
}
}

private static void Validate(string? templateNuGetConfigPath, string? packageSourceNameForBuiltPackages, string? outputPath)
{
if (string.IsNullOrEmpty(templateNuGetConfigPath))
throw new LogAsErrorException($"{nameof(templateNuGetConfigPath)} is required");
Comment thread
radical marked this conversation as resolved.

if (!File.Exists(templateNuGetConfigPath))
throw new LogAsErrorException($"Cannot find {nameof(templateNuGetConfigPath)}={templateNuGetConfigPath}");

if (string.IsNullOrEmpty(packageSourceNameForBuiltPackages))
throw new LogAsErrorException($"{nameof(packageSourceNameForBuiltPackages)} is required");

if (string.IsNullOrEmpty(outputPath))
throw new LogAsErrorException($"{nameof(outputPath)} is required");

if (Directory.Exists(outputPath))
throw new LogAsErrorException($"{nameof(outputPath)}={outputPath} is a directory, it should be a file");
}

public static void GetNuGetConfig(string templateNuGetConfigPath, string localNuGetsPath, string packageSourceNameForBuiltPackages, string[] nuGetConfigPackageSourceMappings, string outputPath)
{
Validate(templateNuGetConfigPath, packageSourceNameForBuiltPackages, outputPath);

XDocument doc = XDocument.Load(templateNuGetConfigPath);
string xpath = "/configuration/packageSources";
XElement? packageSources = doc.XPathSelectElement(xpath);
if (packageSources is null)
throw new LogAsErrorException($"Could not find {xpath} in {templateNuGetConfigPath}");

var newPackageSourceElement = new XElement("add",
new XAttribute("key", packageSourceNameForBuiltPackages),
new XAttribute("value", $"file://{localNuGetsPath}"));
if (packageSources.LastNode is not null)
{
packageSources.LastNode.AddAfterSelf(newPackageSourceElement);
}
else
{
packageSources.Add(newPackageSourceElement);
}

if (nuGetConfigPackageSourceMappings.Length > 0)
{
string mappingXpath = "/configuration/packageSourceMapping";
XElement? packageSourceMapping = doc.XPathSelectElement(mappingXpath);
if (packageSourceMapping is null)
{
if (doc.Root is null)
throw new LogAsErrorException($"Could not find root element in {templateNuGetConfigPath}");

packageSourceMapping = new XElement("packageSourceMapping");
doc.Root.Add(packageSourceMapping);
}

var newPackageSourceMappingElement = new XElement("packageSource",
new XAttribute("key", packageSourceNameForBuiltPackages),
nuGetConfigPackageSourceMappings.Select
(pattern => new XElement("package", new XAttribute("pattern", pattern))));
if (packageSourceMapping.FirstNode is not null)
{
packageSourceMapping.FirstNode?.AddBeforeSelf(newPackageSourceMappingElement);
}
else
{
packageSourceMapping.Add(newPackageSourceMappingElement);
}
}

using var xw = XmlWriter.Create(outputPath, new XmlWriterSettings { Indent = true, NewLineHandling = NewLineHandling.None, Encoding = Encoding.UTF8 });
doc.WriteTo(xw);
xw.Close();
}
}