Skip to content

Commit 9927abb

Browse files
authored
Use workload sets as per user request (#37681)
This PR should enable installing and updating via workload sets. In particular, if you have mode set to workload sets (and don't use a rollback file), it will now look for the latest workload set (or latest stable if you don't specify to use previews) and install/update to that rather than updating manifests individually. It now also supports dotnet workload update --version x where it will update the workload set version x. Finally, it now will display the workload set it is using if it's set to use workload sets in the install state file when the user runs dotnet workload list. All functionality was manually tested at some point, and there's a new unit test.
1 parent c2d085d commit 9927abb

File tree

84 files changed

+1056
-262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+1056
-262
lines changed

src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ public static class Constants
2828
public static readonly string AnyRid = "any";
2929

3030
public static readonly string RestoreInteractiveOption = "--interactive";
31+
public static readonly string workloadSetVersionFileName = "workloadVersion.txt";
3132
}
3233
}

src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ public static bool TryDeleteDirectory(string directoryPath)
9797
}
9898
}
9999

100+
/// <summary>
101+
/// Deletes the provided file. Then deletes the parent directory if empty
102+
/// and continues to its parent until it fails. Returns whether it succeeded
103+
/// in deleting the file it was intended to delete.
104+
/// </summary>
105+
public static bool DeleteFileAndEmptyParents(string path)
106+
{
107+
if (!File.Exists(path))
108+
{
109+
return false;
110+
}
111+
112+
File.Delete(path);
113+
var dir = Path.GetDirectoryName(path);
114+
115+
while (!Directory.EnumerateFileSystemEntries(dir).Any())
116+
{
117+
Directory.Delete(dir);
118+
dir = Path.GetDirectoryName(dir);
119+
}
120+
121+
return !File.Exists(path);
122+
}
123+
100124
/// <summary>
101125
/// Returns childItem relative to directory, with Path.DirectorySeparatorChar as separator
102126
/// </summary>

src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,21 @@ public InstallResponseMessage SendUpdateWorkloadModeRequest(SdkFeatureBand sdkFe
192192
UseWorkloadSets = newMode,
193193
});
194194
}
195+
196+
/// <summary>
197+
/// Send an <see cref="InstallRequestMessage"/> to adjust the workload set version used for installing and updating workloads
198+
/// </summary>
199+
/// <param name="sdkFeatureBand">The SDK feature band of the install state file to write</param>
200+
/// <param name="newVersion">The workload set version</param>
201+
/// <returns></returns>
202+
public InstallResponseMessage SendUpdateWorkloadSetRequest(SdkFeatureBand sdkFeatureBand, string newVersion)
203+
{
204+
return Send(new InstallRequestMessage
205+
{
206+
RequestType = InstallRequestType.AdjustWorkloadSetVersion,
207+
SdkFeatureBand = sdkFeatureBand.ToString(),
208+
WorkloadSetVersion = newVersion,
209+
});
210+
}
195211
}
196212
}

src/Cli/dotnet/Installer/Windows/InstallRequestMessage.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ public bool UseWorkloadSets
128128
get; set;
129129
}
130130

131+
/// <summary>
132+
/// The workload set version
133+
/// </summary>
134+
public string WorkloadSetVersion
135+
{
136+
get; set;
137+
}
138+
131139
/// <summary>
132140
/// Converts a deserialized array of bytes into an <see cref="InstallRequestMessage"/>.
133141
/// </summary>

src/Cli/dotnet/Installer/Windows/InstallRequestType.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,10 @@ public enum InstallRequestType
6969
/// Changes the workload mode
7070
/// </summary>
7171
AdjustWorkloadMode,
72+
73+
/// <summary>
74+
/// Changes the workload set version
75+
/// </summary>
76+
AdjustWorkloadSetVersion,
7277
}
7378
}

src/Cli/dotnet/commands/InstallingWorkloadCommand.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal abstract class InstallingWorkloadCommand : WorkloadCommandBase
3434
protected readonly SdkFeatureBand _sdkFeatureBand;
3535
protected readonly ReleaseVersion _targetSdkVersion;
3636
protected readonly string _fromRollbackDefinition;
37+
protected string _workloadSetVersion;
3738
protected readonly PackageSourceLocation _packageSourceLocation;
3839
protected readonly IWorkloadResolverFactory _workloadResolverFactory;
3940
protected IWorkloadResolver _workloadResolver;
@@ -96,6 +97,53 @@ protected static Dictionary<string, string> GetInstallStateContents(IEnumerable<
9697
manifestVersionUpdates.Select(update => new WorkloadManifestInfo(update.ManifestId.ToString(), update.NewVersion.ToString(), /* We don't actually use the directory here */ string.Empty, update.NewFeatureBand))
9798
).ToDictionaryForJson();
9899

100+
public static bool ShouldUseWorkloadSetMode(SdkFeatureBand sdkFeatureBand, string dotnetDir)
101+
{
102+
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(sdkFeatureBand, dotnetDir), "default.json");
103+
var installStateContents = File.Exists(path) ? InstallStateContents.FromString(File.ReadAllText(path)) : new InstallStateContents();
104+
return installStateContents.UseWorkloadSets ?? false;
105+
}
106+
107+
protected IEnumerable<ManifestVersionUpdate> HandleWorkloadUpdateFromVersion(ITransactionContext context, DirectoryPath? offlineCache)
108+
{
109+
// Ensure workload set mode is set to 'workloadset'
110+
// Do not skip checking the mode first, as setting it triggers
111+
// an admin authorization popup for MSI-based installs.
112+
if (!ShouldUseWorkloadSetMode(_sdkFeatureBand, _dotnetPath))
113+
{
114+
_workloadInstaller.UpdateInstallMode(_sdkFeatureBand, true);
115+
}
116+
117+
_workloadManifestUpdater.DownloadWorkloadSet(_workloadSetVersion, offlineCache);
118+
return InstallWorkloadSet(context);
119+
}
120+
121+
public IEnumerable<ManifestVersionUpdate> InstallWorkloadSet(ITransactionContext context)
122+
{
123+
var advertisingPackagePath = Path.Combine(_userProfileDir, "sdk-advertising", _sdkFeatureBand.ToString(), "microsoft.net.workloads");
124+
if (File.Exists(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName)))
125+
{
126+
// This file isn't created in tests.
127+
PrintWorkloadSetTransition(File.ReadAllText(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName)));
128+
}
129+
var workloadSetPath = _workloadInstaller.InstallWorkloadSet(context, advertisingPackagePath);
130+
var files = Directory.EnumerateFiles(workloadSetPath, "*.workloadset.json");
131+
return _workloadManifestUpdater.ParseRollbackDefinitionFiles(files);
132+
}
133+
134+
private void PrintWorkloadSetTransition(string newVersion)
135+
{
136+
var currentVersion = _workloadResolver.GetWorkloadVersion();
137+
if (currentVersion == null)
138+
{
139+
Reporter.WriteLine(string.Format(Strings.NewWorkloadSet, newVersion));
140+
}
141+
else
142+
{
143+
Reporter.WriteLine(string.Format(Strings.WorkloadSetUpgrade, currentVersion, newVersion));
144+
}
145+
}
146+
99147
protected async Task<List<WorkloadDownload>> GetDownloads(IEnumerable<WorkloadId> workloadIds, bool skipManifestUpdate, bool includePreview, string downloadFolder = null)
100148
{
101149
List<WorkloadDownload> ret = new();
@@ -202,6 +250,11 @@ internal static class InstallingWorkloadCommandParser
202250
Hidden = true
203251
};
204252

253+
public static readonly CliOption<string> WorkloadSetVersionOption = new("--version")
254+
{
255+
Description = Strings.WorkloadSetVersionOptionDescription
256+
};
257+
205258
public static readonly CliOption<bool> PrintDownloadLinkOnlyOption = new("--print-download-link-only")
206259
{
207260
Description = Strings.PrintDownloadLinkOnlyDescription,

src/Cli/dotnet/commands/dotnet-workload/InstallStateContents.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Text.Json;
55
using System.Text.Json.Serialization;
66

7+
#pragma warning disable CS8632
8+
79
namespace Microsoft.DotNet.Workloads.Workload
810
{
911
internal class InstallStateContents
@@ -12,17 +14,21 @@ internal class InstallStateContents
1214
public bool? UseWorkloadSets { get; set; }
1315

1416
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
15-
public Dictionary<string, string> Manifests { get; set; }
17+
public Dictionary<string, string>? Manifests { get; set; }
18+
19+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
20+
public string? WorkloadVersion { get; set; }
1621

1722
private static readonly JsonSerializerOptions s_options = new()
1823
{
1924
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
2025
WriteIndented = true,
26+
AllowTrailingCommas = true,
2127
};
2228

2329
public static InstallStateContents FromString(string contents)
2430
{
25-
return JsonSerializer.Deserialize<InstallStateContents>(contents, s_options);
31+
return JsonSerializer.Deserialize<InstallStateContents>(contents, s_options) ?? new InstallStateContents();
2632
}
2733

2834
public static InstallStateContents FromPath(string path)
@@ -35,4 +41,6 @@ public override string ToString()
3541
return JsonSerializer.Serialize<InstallStateContents>(this, s_options);
3642
}
3743
}
38-
}
44+
}
45+
46+
#pragma warning restore CS8632

src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal class WorkloadInfoHelper : IWorkloadInfoHelper
1717
{
1818
public readonly SdkFeatureBand _currentSdkFeatureBand;
1919
private readonly string _targetSdkVersion;
20+
public string DotnetPath { get; }
2021

2122
public WorkloadInfoHelper(
2223
bool isInteractive,
@@ -30,20 +31,20 @@ public WorkloadInfoHelper(
3031
string userProfileDir = null,
3132
IWorkloadResolver workloadResolver = null)
3233
{
33-
string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
34+
DotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
3435
ReleaseVersion currentSdkReleaseVersion = new(currentSdkVersion ?? Product.Version);
3536
_currentSdkFeatureBand = new SdkFeatureBand(currentSdkReleaseVersion);
3637

3738
_targetSdkVersion = targetSdkVersion;
3839
userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath;
3940
ManifestProvider =
40-
new SdkDirectoryWorkloadManifestProvider(dotnetPath,
41+
new SdkDirectoryWorkloadManifestProvider(DotnetPath,
4142
string.IsNullOrWhiteSpace(_targetSdkVersion)
4243
? currentSdkReleaseVersion.ToString()
4344
: _targetSdkVersion,
4445
userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory));
4546
WorkloadResolver = workloadResolver ?? NET.Sdk.WorkloadManifestReader.WorkloadResolver.Create(
46-
ManifestProvider, dotnetPath,
47+
ManifestProvider, DotnetPath,
4748
currentSdkReleaseVersion.ToString(), userProfileDir);
4849

4950
var restoreConfig = new RestoreActionConfig(Interactive: isInteractive);

src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public override int Execute()
5959

6060
private void ExecuteGarbageCollection()
6161
{
62-
_workloadInstaller.GarbageCollect(workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion),
62+
_workloadInstaller.GarbageCollect(workloadVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadVersion),
6363
cleanAllPacks: _cleanAll);
6464

6565
DisplayUninstallableVSWorkloads();

src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
using NuGet.Common;
1414
using NuGet.Versioning;
1515
using static Microsoft.NET.Sdk.WorkloadManifestReader.WorkloadResolver;
16-
16+
using PathUtility = Microsoft.DotNet.Tools.Common.PathUtility;
1717

1818
namespace Microsoft.DotNet.Workloads.Workload.Install
1919
{
@@ -85,6 +85,31 @@ IEnumerable<PackInfo> GetPacksInWorkloads(IEnumerable<WorkloadId> workloadIds)
8585
return packs;
8686
}
8787

88+
public string InstallWorkloadSet(ITransactionContext context, string advertisingPackagePath)
89+
{
90+
var workloadVersion = File.ReadAllText(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName));
91+
var workloadSetPath = Path.Combine(_dotnetDir, "sdk-manifests", _sdkFeatureBand.ToString(), "workloadsets", workloadVersion);
92+
context.Run(
93+
action: () =>
94+
{
95+
Directory.CreateDirectory(workloadSetPath);
96+
97+
foreach (var file in Directory.EnumerateFiles(advertisingPackagePath))
98+
{
99+
File.Copy(file, Path.Combine(workloadSetPath, Path.GetFileName(file)), overwrite: true);
100+
}
101+
},
102+
rollback: () =>
103+
{
104+
foreach (var file in Directory.EnumerateFiles(workloadSetPath))
105+
{
106+
PathUtility.DeleteFileAndEmptyParents(file);
107+
}
108+
});
109+
110+
return workloadSetPath;
111+
}
112+
88113
public void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null)
89114
{
90115
var packInfos = GetPacksInWorkloads(workloadIds);
@@ -452,6 +477,15 @@ public void GarbageCollect(Func<string, IWorkloadResolver> getResolverForWorkloa
452477

453478
}
454479

480+
public void AdjustWorkloadSetInInstallState(SdkFeatureBand sdkFeatureBand, string workloadVersion)
481+
{
482+
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _dotnetDir), "default.json");
483+
Directory.CreateDirectory(Path.GetDirectoryName(path));
484+
var installStateContents = InstallStateContents.FromPath(path);
485+
installStateContents.WorkloadVersion = workloadVersion;
486+
File.WriteAllText(path, installStateContents.ToString());
487+
}
488+
455489
public void RemoveManifestsFromInstallState(SdkFeatureBand sdkFeatureBand)
456490
{
457491
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _dotnetDir), "default.json");
@@ -509,7 +543,14 @@ public void Shutdown()
509543

510544
public PackageId GetManifestPackageId(ManifestId manifestId, SdkFeatureBand featureBand)
511545
{
512-
return new PackageId($"{manifestId}.Manifest-{featureBand}");
546+
if (manifestId.ToString().Equals("Microsoft.NET.Workloads", StringComparison.OrdinalIgnoreCase))
547+
{
548+
return new PackageId($"{manifestId}.{featureBand}");
549+
}
550+
else
551+
{
552+
return new PackageId($"{manifestId}.Manifest-{featureBand}");
553+
}
513554
}
514555

515556
public async Task ExtractManifestAsync(string nupkgPath, string targetPath)

0 commit comments

Comments
 (0)