diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 0177d78daa08..be64ed44ecc6 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -5,7 +5,7 @@ trigger: branches: include: - main - - release/9.0.1xx + - release/9.0.2xx - internal/release/* - exp/* diff --git a/NuGet.config b/NuGet.config index a6f37020b08f..7f7ab1a0ca1d 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,6 +4,8 @@ + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7ed68812bb6a..b34c13b0965a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -660,14 +660,14 @@ aren't shipping, or those extensions packages depend on aspnetcore packages that won't ship. However, given the cost of maintaining this coherency path is high. This being toolset means that aspire is responsible for its own coherency. --> - + https://github.com/dotnet/aspire - b88ce9e7cb0430fb0b4e2d018f13694a4c733289 + 137e8dcae0a7b22c05f48c4e7a5d36fe3f00a8d7 - + https://github.com/dotnet/aspire - b88ce9e7cb0430fb0b4e2d018f13694a4c733289 + 137e8dcae0a7b22c05f48c4e7a5d36fe3f00a8d7 diff --git a/eng/Versions.props b/eng/Versions.props index 2dfbb229c183..cb1b7edfd783 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -362,7 +362,7 @@ 8.0.100 - 9.0.0-preview.4.24456.4 + 8.2.1 9.0.100-preview.6 9.0.0-preview.6.24327.7 34.99.0-preview.6.340 diff --git a/sdk.sln b/sdk.sln index 0b5a7846a285..9e6140637b8b 100644 --- a/sdk.sln +++ b/sdk.sln @@ -510,6 +510,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.WebTools.AspireSe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Sdk.Compilers.Toolset", "src\Microsoft.Net.Sdk.Compilers.Toolset\Microsoft.Net.Sdk.Compilers.Toolset.csproj", "{FA579C03-2EB4-4D47-88EE-BFF339E96FAF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WebTools.AspireService.Tests", "test\Microsoft.WebTools.AspireService.Tests\Microsoft.WebTools.AspireService.Tests.csproj", "{1F0B4B3C-DC88-4740-B04F-1707102E9930}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -968,6 +970,10 @@ Global {FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Release|Any CPU.Build.0 = Release|Any CPU + {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1146,6 +1152,7 @@ Global {19014C60-F87C-4CC7-AC0F-C41B6126EBCE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91} {94C8526E-DCC2-442F-9868-3DD0BA2688BE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91} {FA579C03-2EB4-4D47-88EE-BFF339E96FAF} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E} + {1F0B4B3C-DC88-4740-B04F-1707102E9930} = {580D1AE7-AA8F-4912-8B76-105594E00B3B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6} @@ -1153,6 +1160,7 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5 + src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5 src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5 diff --git a/src/BuiltInTools/AspireService/AspireServerService.cs b/src/BuiltInTools/AspireService/AspireServerService.cs index d777ce951154..3b620131f73f 100644 --- a/src/BuiltInTools/AspireService/AspireServerService.cs +++ b/src/BuiltInTools/AspireService/AspireServerService.cs @@ -1,18 +1,12 @@ // 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.Collections.Generic; -using System.Diagnostics; using System.Net; using System.Net.WebSockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; diff --git a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs index 189a88c48ed5..a0b0f7766d48 100644 --- a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs +++ b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs @@ -1,7 +1,6 @@ // 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.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -35,7 +34,11 @@ public static X509Certificate2 GenerateCert() // The file will be automatically generated by the following call and disposed when the returned cert is disposed. using (cert) { +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), password: null, X509KeyStorageFlags.UserKeySet); +#else return new X509Certificate2(cert.Export(X509ContentType.Pfx), "", X509KeyStorageFlags.UserKeySet); +#endif } } else diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs index 62182db3b0bd..50301fdbf15c 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs @@ -82,7 +82,7 @@ private UpdateHandlerActions GetMetadataUpdateHandlerActions() var handlerActions = new UpdateHandlerActions(); foreach (var assembly in sortedAssemblies) { - foreach (var attr in assembly.GetCustomAttributesData()) + foreach (var attr in TryGetCustomAttributesData(assembly)) { // Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to // define their own copy without having to cross-compile. @@ -106,6 +106,25 @@ private UpdateHandlerActions GetMetadataUpdateHandlerActions() return handlerActions; } + private IList TryGetCustomAttributesData(Assembly assembly) + { + try + { + return assembly.GetCustomAttributesData(); + } + catch (Exception e) + { + // In cross-platform scenarios, such as debugging in VS through WSL, Roslyn + // runs on Windows, and the agent runs on Linux. Assemblies accessible to Windows + // may not be available or loaded on linux (such as WPF's assemblies). + // In such case, we can ignore the assemblies and continue enumerating handlers for + // the rest of the assemblies of current domain. + _log($"'{assembly.FullName}' is not loaded ({e.Message})"); + + return new List(); + } + } + internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handlerType) { bool methodFound = false; diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs index 0941b5b89621..1968f7d21aa3 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -26,9 +26,11 @@ public static void Initialize() // When launching the application process dotnet-watch sets Hot Reload environment variables via CLI environment directives (dotnet [env:X=Y] run). // Currently, the CLI parser sets the env variables to the dotnet.exe process itself, rather then to the target process. // This may cause the dotnet.exe process to connect to the named pipe and break it for the target process. - if (Path.ChangeExtension(processPath, ".exe") != Path.ChangeExtension(s_targetProcessPath, ".exe")) + var processExe = Path.ChangeExtension(processPath, ".exe"); + var expectedExe = Path.ChangeExtension(s_targetProcessPath, ".exe"); + if (!string.Equals(processExe, expectedExe, Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) { - Log($"Ignoring process '{processPath}', expecting '{s_targetProcessPath}'"); + Log($"Ignoring process '{processExe}', expecting '{expectedExe}'"); return; } diff --git a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs index 785219c3879e..d2f57ce7eb34 100644 --- a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs @@ -27,7 +27,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment(); - FileItem? changedFile = null; + ChangedFile? changedFile = null; var buildEvaluator = new BuildEvaluator(Context, RootFileSetFactory); await using var browserConnector = new BrowserConnector(Context); @@ -86,7 +86,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, processExitedSource: null, combinedCancellationSource.Token); - Task fileSetTask; + Task fileSetTask; Task finishedTask; while (true) @@ -94,9 +94,9 @@ public override async Task WatchAsync(CancellationToken cancellationToken) fileSetTask = fileSetWatcher.GetChangedFileAsync(startedWatching: null, combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); - if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result is FileItem fileItem) + if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue) { - if (await staticFileHandler.HandleFileChangesAsync([fileItem], combinedCancellationSource.Token)) + if (await staticFileHandler.HandleFileChangesAsync([fileSetTask.Result.Value], combinedCancellationSource.Token)) { // We're able to handle the file change event without doing a full-rebuild. continue; @@ -131,7 +131,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) Debug.Assert(finishedTask == fileSetTask); changedFile = fileSetTask.Result; Debug.Assert(changedFile != null, "ChangedFile should only be null when cancelled"); - Context.Reporter.Output($"File changed: {changedFile.Value.FilePath}"); + Context.Reporter.Output($"File changed: {changedFile.Value.Item.FilePath}"); } } } diff --git a/src/BuiltInTools/dotnet-watch/FileItem.cs b/src/BuiltInTools/dotnet-watch/FileItem.cs index 71779898168a..7fc91cd6fa1c 100644 --- a/src/BuiltInTools/dotnet-watch/FileItem.cs +++ b/src/BuiltInTools/dotnet-watch/FileItem.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.DotNet.Watcher.Internal; + namespace Microsoft.DotNet.Watcher { - internal readonly struct FileItem + internal readonly record struct FileItem { public string FilePath { get; init; } @@ -14,7 +16,7 @@ internal readonly struct FileItem public string? StaticWebAssetPath { get; init; } - public bool IsNewFile { get; init; } + public ChangeKind Change { get; init; } public bool IsStaticFile => StaticWebAssetPath != null; } diff --git a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs index ad7599378ee2..0025fbbbcbf5 100644 --- a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs +++ b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs @@ -46,7 +46,7 @@ public IReadOnlyList GetProcessArguments(int iteration) return [context.RootProjectOptions.Command, .. context.RootProjectOptions.CommandArguments]; } - public async ValueTask EvaluateAsync(FileItem? changedFile, CancellationToken cancellationToken) + public async ValueTask EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken) { if (context.EnvironmentOptions.SuppressMSBuildIncrementalism) { @@ -54,7 +54,7 @@ public async ValueTask EvaluateAsync(FileItem? changedFile, Ca return _evaluationResult = await CreateEvaluationResult(cancellationToken); } - if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile)) + if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile?.Item)) { RequiresRevaluation = true; } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs index 17e4c6d27cd8..09871a21655c 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs @@ -1,20 +1,15 @@ // 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.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MSBuild; -using Microsoft.Extensions.Tools.Internal; -using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; -using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; +using Microsoft.DotNet.Watcher.Internal; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools; @@ -106,13 +101,24 @@ ImmutableArray MapDocuments(ProjectId mappedProjectId, IReadOnlyLi }).ToImmutableArray(); } - public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken) + public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken) { var updatedSolution = CurrentSolution; - foreach (var changedFile in changedFiles) + var documentsToRemove = new List(); + + foreach (var (changedFile, change) in changedFiles) { + // when a file is added we reevaluate the project: + Debug.Assert(change != ChangeKind.Add); + var documentIds = updatedSolution.GetDocumentIdsWithFilePath(changedFile.FilePath); + if (change == ChangeKind.Delete) + { + documentsToRemove.AddRange(documentIds); + continue; + } + foreach (var documentId in documentIds) { var textDocument = updatedSolution.GetDocument(documentId) @@ -140,9 +146,17 @@ public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles } } + updatedSolution = RemoveDocuments(updatedSolution, documentsToRemove); + await ReportSolutionFilesAsync(SetCurrentSolution(updatedSolution), cancellationToken); } + private static Solution RemoveDocuments(Solution solution, IEnumerable ids) + => solution + .RemoveDocuments(ids.Where(id => solution.GetDocument(id) != null).ToImmutableArray()) + .RemoveAdditionalDocuments(ids.Where(id => solution.GetAdditionalDocument(id) != null).ToImmutableArray()) + .RemoveAnalyzerConfigDocuments(ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null).ToImmutableArray()); + private static async ValueTask GetSourceTextAsync(string filePath, CancellationToken cancellationToken) { var zeroLengthRetryPerformed = false; diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs index dfe625075227..3a6961854987 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Diagnostics; using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools @@ -13,14 +14,14 @@ internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap pr { private const string BuildTargetName = "GenerateComputedBuildStaticWebAssets"; - public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken) + public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken) { var projectsToRefresh = new HashSet(); var hasApplicableFiles = false; for (int i = 0; i < files.Count; i++) { - var file = files[i]; + var file = files[i].Item; if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) && !file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal)) diff --git a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs index f8d9b8bffd12..ce53bc8a7ce3 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.CodeAnalysis.StackTraceExplorer; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools @@ -18,13 +19,13 @@ internal sealed class StaticFileHandler(IReporter reporter, ProjectNodeMap proje DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken) + public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken) { var allFilesHandled = true; var refreshRequests = new Dictionary>(); for (int i = 0; i < files.Count; i++) { - var file = files[i]; + var file = files[i].Item; if (file.StaticWebAssetPath is null) { diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs index ee09b3c257df..24b543ebd9b3 100644 --- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs @@ -74,7 +74,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke HotReloadFileSetWatcher? fileSetWatcher = null; EvaluationResult? evaluationResult = null; RunningProject? rootRunningProject = null; - Task? fileSetWatcherTask = null; + Task? fileSetWatcherTask = null; try { @@ -178,7 +178,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke // When a new file is added we need to run design-time build to find out // what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.). // We don't need to rebuild and restart the application though. - if (changedFiles.Any(f => f.IsNewFile)) + if (changedFiles.Any(f => f.Change is ChangeKind.Add)) { Context.Reporter.Verbose("File addition triggered re-evaluation."); @@ -195,9 +195,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke // update files in the change set with new evaluation info: for (int i = 0; i < changedFiles.Length; i++) { - if (evaluationResult.Files.TryGetValue(changedFiles[i].FilePath, out var evaluatedFile)) + if (evaluationResult.Files.TryGetValue(changedFiles[i].Item.FilePath, out var evaluatedFile)) { - changedFiles[i] = evaluatedFile; + changedFiles[i] = changedFiles[i] with { Item = evaluatedFile }; } } @@ -336,24 +336,43 @@ await Task.WhenAll( } } - private void ReportFileChanges(IReadOnlyList fileItems) + private void ReportFileChanges(IReadOnlyList changedFiles) { - Report(added: true); - Report(added: false); + Report(kind: ChangeKind.Add); + Report(kind: ChangeKind.Update); + Report(kind: ChangeKind.Delete); - void Report(bool added) + void Report(ChangeKind kind) { - var items = fileItems.Where(item => item.IsNewFile == added).ToArray(); + var items = changedFiles.Where(item => item.Change == kind).ToArray(); if (items is not []) { - Context.Reporter.Output(GetMessage(items, added)); + Context.Reporter.Output(GetMessage(items, kind)); } } - string GetMessage(IReadOnlyList items, bool added) - => items is [var item] - ? (added ? "File added: " : "File changed: ") + GetRelativeFilePath(item.FilePath) - : (added ? "Files added: " : "Files changed: ") + string.Join(", ", items.Select(f => GetRelativeFilePath(f.FilePath))); + string GetMessage(IReadOnlyList items, ChangeKind kind) + => items is [{Item: var item }] + ? GetSingularMessage(kind) + ": " + GetRelativeFilePath(item.FilePath) + : GetPluralMessage(kind) + ": " + string.Join(", ", items.Select(f => GetRelativeFilePath(f.Item.FilePath))); + + static string GetSingularMessage(ChangeKind kind) + => kind switch + { + ChangeKind.Update => "File updated", + ChangeKind.Add => "File added", + ChangeKind.Delete => "File deleted", + _ => throw new InvalidOperationException() + }; + + static string GetPluralMessage(ChangeKind kind) + => kind switch + { + ChangeKind.Update => "Files updated", + ChangeKind.Add => "Files added", + ChangeKind.Delete => "Files deleted", + _ => throw new InvalidOperationException() + }; } private async ValueTask EvaluateRootProjectAsync(CancellationToken cancellationToken) diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs index b844f8da4fa8..3a339bf63fb1 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs @@ -10,7 +10,7 @@ internal sealed class FileWatcher(IReadOnlyDictionary fileSet, private readonly Dictionary _watchers = []; private bool _disposed; - public event Action? OnFileChange; + public event Action? OnFileChange; public void Dispose() { @@ -73,9 +73,9 @@ private void WatcherErrorHandler(object? sender, Exception error) } } - private void WatcherChangedHandler(object? sender, (string changedPath, bool newFile) args) + private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args) { - OnFileChange?.Invoke(args.changedPath, args.newFile); + OnFileChange?.Invoke(args.changedPath, args.kind); } private void DisposeWatcher(string directory) @@ -101,22 +101,22 @@ private void EnsureNotDisposed() private static string EnsureTrailingSlash(string path) => (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path; - public async Task GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken) + public async Task GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken) { StartWatching(); - var fileChangedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var fileChangedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); cancellationToken.Register(() => fileChangedSource.TrySetResult(null)); - void FileChangedCallback(string path, bool newFile) + void FileChangedCallback(string path, ChangeKind kind) { if (fileSet.TryGetValue(path, out var fileItem)) { - fileChangedSource.TrySetResult(fileItem); + fileChangedSource.TrySetResult(new ChangedFile(fileItem, kind)); } } - FileItem? changedFile; + ChangedFile? changedFile; OnFileChange += FileChangedCallback; try diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs new file mode 100644 index 000000000000..5fef3b698624 --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watcher.Internal; + +internal enum ChangeKind +{ + Update, + Add, + Delete +} + +internal readonly record struct ChangedFile(FileItem Item, ChangeKind Change); diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs index ca622206e933..7040fe1a0763 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs @@ -33,7 +33,7 @@ internal DotnetFileWatcher(string watchedDirectory, Func? OnFileChange; + public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange; public event EventHandler? OnError; @@ -78,10 +78,10 @@ private void WatcherRenameHandler(object sender, RenamedEventArgs e) return; } - Logger?.Invoke("Rename"); + Logger?.Invoke($"Renamed '{e.OldFullPath}' to '{e.FullPath}'."); - NotifyChange(e.OldFullPath, newFile: false); - NotifyChange(e.FullPath, newFile: true); + NotifyChange(e.OldFullPath, ChangeKind.Delete); + NotifyChange(e.FullPath, ChangeKind.Add); if (Directory.Exists(e.FullPath)) { @@ -89,12 +89,23 @@ private void WatcherRenameHandler(object sender, RenamedEventArgs e) { // Calculated previous path of this moved item. var oldLocation = Path.Combine(e.OldFullPath, newLocation.Substring(e.FullPath.Length + 1)); - NotifyChange(oldLocation, newFile: false); - NotifyChange(newLocation, newFile: true); + NotifyChange(oldLocation, ChangeKind.Delete); + NotifyChange(newLocation, ChangeKind.Add); } } } + private void WatcherDeletedHandler(object sender, FileSystemEventArgs e) + { + if (_disposed) + { + return; + } + + Logger?.Invoke($"Deleted '{e.FullPath}'."); + NotifyChange(e.FullPath, ChangeKind.Delete); + } + private void WatcherChangeHandler(object sender, FileSystemEventArgs e) { if (_disposed) @@ -102,8 +113,8 @@ private void WatcherChangeHandler(object sender, FileSystemEventArgs e) return; } - Logger?.Invoke("Change"); - NotifyChange(e.FullPath, newFile: false); + Logger?.Invoke($"Updated '{e.FullPath}'."); + NotifyChange(e.FullPath, ChangeKind.Update); } private void WatcherAddedHandler(object sender, FileSystemEventArgs e) @@ -113,14 +124,14 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e) return; } - Logger?.Invoke("Added"); - NotifyChange(e.FullPath, newFile: true); + Logger?.Invoke($"Added '{e.FullPath}'."); + NotifyChange(e.FullPath, ChangeKind.Add); } - private void NotifyChange(string fullPath, bool newFile) + private void NotifyChange(string fullPath, ChangeKind kind) { // Only report file changes - OnFileChange?.Invoke(this, (fullPath, newFile)); + OnFileChange?.Invoke(this, (fullPath, kind)); } private void CreateFileSystemWatcher() @@ -140,7 +151,7 @@ private void CreateFileSystemWatcher() _fileSystemWatcher.IncludeSubdirectories = true; _fileSystemWatcher.Created += WatcherAddedHandler; - _fileSystemWatcher.Deleted += WatcherChangeHandler; + _fileSystemWatcher.Deleted += WatcherDeletedHandler; _fileSystemWatcher.Changed += WatcherChangeHandler; _fileSystemWatcher.Renamed += WatcherRenameHandler; _fileSystemWatcher.Error += WatcherErrorHandler; @@ -156,7 +167,7 @@ private void DisposeInnerWatcher() _fileSystemWatcher.EnableRaisingEvents = false; _fileSystemWatcher.Created -= WatcherAddedHandler; - _fileSystemWatcher.Deleted -= WatcherChangeHandler; + _fileSystemWatcher.Deleted -= WatcherDeletedHandler; _fileSystemWatcher.Changed -= WatcherChangeHandler; _fileSystemWatcher.Renamed -= WatcherRenameHandler; _fileSystemWatcher.Error -= WatcherErrorHandler; diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs index f75c7e3e1481..ebdef49913ff 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs @@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watcher.Internal { internal interface IFileSystemWatcher : IDisposable { - event EventHandler<(string filePath, bool newFile)> OnFileChange; + event EventHandler<(string filePath, ChangeKind kind)> OnFileChange; event EventHandler OnError; diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs index da7630b71eb8..2df2004ee84b 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs @@ -15,7 +15,7 @@ internal class PollingFileWatcher : IFileSystemWatcher private Dictionary _knownEntities = new(); private Dictionary _tempDictionary = new(); - private Dictionary _changes = new(); + private Dictionary _changes = new(); private Thread _pollingThread; private bool _raiseEvents; @@ -40,7 +40,7 @@ public PollingFileWatcher(string watchedDirectory) _pollingThread.Start(); } - public event EventHandler<(string filePath, bool newFile)>? OnFileChange; + public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange; #pragma warning disable CS0067 // not used public event EventHandler? OnError; @@ -106,8 +106,8 @@ private void CheckForChangedFiles() if (!_knownEntities.ContainsKey(fullFilePath)) { - // New file - RecordChange(f, isNewFile: true); + // New file or directory + RecordChange(f, ChangeKind.Add); } else { @@ -115,10 +115,11 @@ private void CheckForChangedFiles() try { - if (fileMeta.FileInfo.LastWriteTime != f.LastWriteTime) + if (!fileMeta.FileInfo.Attributes.HasFlag(FileAttributes.Directory) && + fileMeta.FileInfo.LastWriteTime != f.LastWriteTime) { // File changed - RecordChange(f, isNewFile: false); + RecordChange(f, ChangeKind.Update); } _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, foundAgain: true); @@ -136,8 +137,8 @@ private void CheckForChangedFiles() { if (!file.Value.FoundAgain) { - // File deleted - RecordChange(file.Value.FileInfo, isNewFile: false); + // File or directory deleted + RecordChange(file.Value.FileInfo, ChangeKind.Delete); } } @@ -148,7 +149,7 @@ private void CheckForChangedFiles() _tempDictionary.Clear(); } - private void RecordChange(FileSystemInfo fileInfo, bool isNewFile) + private void RecordChange(FileSystemInfo fileInfo, ChangeKind kind) { if (_changes.ContainsKey(fileInfo.FullName) || fileInfo.FullName.Equals(_watchedDirectory.FullName, StringComparison.Ordinal)) @@ -156,18 +157,15 @@ private void RecordChange(FileSystemInfo fileInfo, bool isNewFile) return; } - _changes.Add(fileInfo.FullName, isNewFile); + _changes.Add(fileInfo.FullName, kind); - if (fileInfo.FullName != _watchedDirectory.FullName) + if (fileInfo is FileInfo { Directory: { } directory }) { - if (fileInfo is FileInfo { Directory: { } directory }) - { - RecordChange(directory, isNewFile: false); - } - else if (fileInfo is DirectoryInfo { Parent: { } parent }) - { - RecordChange(parent, isNewFile: false); - } + RecordChange(directory, ChangeKind.Update); + } + else if (fileInfo is DirectoryInfo { Parent: { } parent }) + { + RecordChange(parent, ChangeKind.Update); } } @@ -202,14 +200,14 @@ private static void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action fileSet, DateTime buildCompletionTime, IReporter reporter) : IDisposable { private static readonly TimeSpan s_debounceInterval = TimeSpan.FromMilliseconds(50); + private static readonly DateTime s_fileNotExistFileTime = DateTime.FromFileTime(0); private readonly FileWatcher _fileWatcher = new(fileSet, reporter); private readonly object _changedFilesLock = new(); - private readonly ConcurrentDictionary _changedFiles = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _changedFiles = new(StringComparer.Ordinal); - private TaskCompletionSource? _tcs; + private TaskCompletionSource? _tcs; private bool _initialized; private bool _disposed; @@ -65,7 +67,7 @@ private void EnsureInitialized() continue; } - FileItem[] changedFiles; + ChangedFile[] changedFiles; lock (_changedFilesLock) { changedFiles = _changedFiles.Values.ToArray(); @@ -82,7 +84,7 @@ private void EnsureInitialized() }, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); - void FileChangedCallback(string path, bool newFile) + void FileChangedCallback(string path, ChangeKind kind) { // only handle file changes: if (Directory.Exists(path)) @@ -90,43 +92,66 @@ void FileChangedCallback(string path, bool newFile) return; } - try + if (kind != ChangeKind.Delete) { - // TODO: Deleted files will be ignored https://github.com/dotnet/sdk/issues/42535 - - // Do not report changes to files that happened during build: - var creationTime = File.GetCreationTimeUtc(path); - var writeTime = File.GetLastWriteTimeUtc(path); - if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks) + try + { + // Do not report changes to files that happened during build: + var creationTime = File.GetCreationTimeUtc(path); + var writeTime = File.GetLastWriteTimeUtc(path); + + if (creationTime == s_fileNotExistFileTime || writeTime == s_fileNotExistFileTime) + { + // file might have been deleted since we received the event + kind = ChangeKind.Delete; + } + else if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks) + { + reporter.Verbose( + $"Ignoring file change during build: {kind} '{path}' " + + $"(created {FormatTimestamp(creationTime)} and written {FormatTimestamp(writeTime)} before {FormatTimestamp(buildCompletionTime)})."); + + return; + } + else if (writeTime > creationTime) + { + reporter.Verbose($"File change: {kind} '{path}' (written {FormatTimestamp(writeTime)} after {FormatTimestamp(buildCompletionTime)})."); + } + else + { + reporter.Verbose($"File change: {kind} '{path}' (created {FormatTimestamp(creationTime)} after {FormatTimestamp(buildCompletionTime)})."); + } + } + catch (Exception e) { - reporter.Verbose($"Ignoring file updated during build: '{path}' ({FormatTimestamp(creationTime)},{FormatTimestamp(writeTime)} < {FormatTimestamp(buildCompletionTime)})."); + reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}."); return; } } - catch (Exception e) + + if (kind == ChangeKind.Delete) { - reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}."); - return; + reporter.Verbose($"File '{path}' deleted after {FormatTimestamp(buildCompletionTime)}."); } - if (newFile) + if (kind == ChangeKind.Add) { lock (_changedFilesLock) { - _changedFiles.TryAdd(path, new FileItem { FilePath = path, IsNewFile = newFile }); + _changedFiles.TryAdd(path, new ChangedFile(new FileItem { FilePath = path }, kind)); } } else if (fileSet.TryGetValue(path, out var fileItem)) { lock (_changedFilesLock) { - _changedFiles.TryAdd(path, fileItem); + _changedFiles.TryAdd(path, new ChangedFile(fileItem, kind)); } } } } - public Task GetChangedFilesAsync(CancellationToken cancellationToken, bool forceWaitForNewUpdate = false) + public Task GetChangedFilesAsync(CancellationToken cancellationToken, bool forceWaitForNewUpdate = false) { EnsureInitialized(); @@ -142,6 +167,6 @@ void FileChangedCallback(string path, bool newFile) } internal static string FormatTimestamp(DateTime time) - => time.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); + => time.ToString("HH:mm:ss.fffffff"); } } diff --git a/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs b/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs index b324abf9122e..8684564e03db 100644 --- a/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs +++ b/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs @@ -3,12 +3,18 @@ using System.Reflection; using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.CommandFactory; +using NuGet.Configuration; namespace Microsoft.DotNet.BuildServer { internal class VBCSCompilerServer : IBuildServer { + private static readonly string s_toolsetPackageName = "microsoft.net.sdk.compilers.toolset"; + private static readonly string s_vbcsCompilerExeFileName = "VBCSCompiler.exe"; + private static readonly string s_shutdownArg = "-shutdown"; + internal static readonly string VBCSCompilerPath = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Roslyn", @@ -28,18 +34,46 @@ public VBCSCompilerServer(ICommandFactory commandFactory = null) public void Shutdown() { - var command = _commandFactory - .Create("exec", new[] { VBCSCompilerPath, "-shutdown" }) - .CaptureStdOut() - .CaptureStdErr(); + List errors = null; + + // Shutdown the compiler from the SDK. + execute(_commandFactory.Create("exec", [VBCSCompilerPath, s_shutdownArg]), ref errors); + + // Shutdown toolset compilers. + var nuGetPackageRoot = SettingsUtility.GetGlobalPackagesFolder(Settings.LoadDefaultSettings(root: null)); + var toolsetPackageDirectory = Path.Join(nuGetPackageRoot, s_toolsetPackageName); + if (Directory.Exists(toolsetPackageDirectory)) + { + foreach (var versionDirectory in Directory.EnumerateDirectories(toolsetPackageDirectory)) + { + var vbcsCompilerPath = Path.Join(versionDirectory, s_vbcsCompilerExeFileName); + if (File.Exists(vbcsCompilerPath)) + { + execute(CommandFactoryUsingResolver.Create(vbcsCompilerPath, [s_shutdownArg]), ref errors); + } + } + } - var result = command.Execute(); - if (result.ExitCode != 0) + if (errors?.Count > 0) { throw new BuildServerException( string.Format( LocalizableStrings.ShutdownCommandFailed, - result.StdErr)); + string.Join(Environment.NewLine, errors))); + } + + static void execute(ICommand command, ref List errors) + { + command = command + .CaptureStdOut() + .CaptureStdErr(); + + var result = command.Execute(); + if (result.ExitCode != 0) + { + errors ??= new List(); + errors.Add(result.StdErr); + } } } } diff --git a/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs b/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs index 0dc61ea54241..ba9e244e77b2 100644 --- a/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs +++ b/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs @@ -224,7 +224,7 @@ public InstallResponseMessage SendGetGlobalJsonWorkloadSetVersionsRequest(SdkFea { return Send(new InstallRequestMessage { - RequestType = InstallRequestType.RecordWorkloadSetInGlobalJson, + RequestType = InstallRequestType.GetGlobalJsonWorkloadSetVersions, SdkFeatureBand = sdkFeatureBand.ToString(), }); } diff --git a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs index 2ce33f5d8158..42b25a77fc10 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs @@ -16,7 +16,8 @@ Task DownloadPackageAsync(PackageId packageId, bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null); + PackageSourceMapping packageSourceMapping = null, + bool isTool = false); Task GetPackageUrl(PackageId packageId, NuGetVersion packageVersion = null, diff --git a/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx b/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx index dc604eb2bc79..b526943144d6 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx +++ b/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx @@ -126,6 +126,9 @@ {0} is not found in NuGet feeds {1}. + + Package {0} is not a .NET tool. + Downloading {0} version {1} failed. diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index 262d6cd2fc29..a861993465c8 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -86,19 +86,45 @@ public async Task DownloadPackageAsync(PackageId packageId, bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping packageSourceMapping = null, + bool isTool = false) { CancellationToken cancellationToken = CancellationToken.None; (var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion, packageSourceLocation, includePreview, includeUnlisted ?? packageVersion is not null, packageSourceMapping).ConfigureAwait(false); - FindPackageByIdResource resource = null; SourceRepository repository = GetSourceRepository(source); - resource = await repository.GetResourceAsync(cancellationToken) - .ConfigureAwait(false); + // TODO: Fix this to use the PackageSearchResourceV3 once https://github.com/NuGet/NuGet.Client/pull/5991 is completed. + if (isTool && await repository.GetResourceAsync().ConfigureAwait(false) is ServiceIndexResourceV3 serviceIndex) + { + // See https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec + var uri = serviceIndex.GetServiceEntries("PackageBaseAddress/3.0.0")[0].Uri; + var queryUri = uri + $"{packageId}/{packageVersion}/{packageId}.nuspec"; + + using HttpClient client = new(new HttpClientHandler() { CheckCertificateRevocationList = true }); + using HttpResponseMessage response = await client.GetAsync(queryUri).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + string nuspec = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + XDocument doc = XDocument.Parse(nuspec); + + if (!ToolPackageInstance.IsToolPackage(doc)) + { + throw new GracefulException(string.Format(LocalizableStrings.NotATool, packageId)); + } + } + else + { + throw new GracefulException(string.Format(LocalizableStrings.NotATool, packageId)); + } + } + FindPackageByIdResource resource = await repository.GetResourceAsync(cancellationToken) + .ConfigureAwait(false); if (resource == null) { throw new NuGetPackageNotFoundException( diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf index e5b5123b171a..d87b1db20e13 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf @@ -39,6 +39,11 @@ Balíček {0} se nenašel v informačních kanálech NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf index b0f68edf07a9..b0d18b15fa40 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf @@ -39,6 +39,11 @@ "{0}" wurde in NuGet-Feeds "{1}" nicht gefunden. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf index 2261022a005b..c77bb97e1a70 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf @@ -39,6 +39,11 @@ No se encuentra {0} en las fuentes de NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf index c7a5d791edc3..61391544e180 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf @@ -39,6 +39,11 @@ {0} est introuvable dans les flux NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf index dafa443d72db..2f79a93f75a4 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf @@ -39,6 +39,11 @@ {0} non è stato trovato nei feed NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf index fc1e0d3848e2..d89d1cdc10c5 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf @@ -39,6 +39,11 @@ {0} が NuGet フィード {1} に見つかりません。 + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf index 0530ae8622f5..eb7f0181a5fe 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf @@ -39,6 +39,11 @@ NuGet 피드 {1} 에서 {0}을(를) 찾을 수 없습니다. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf index 2513fb391d78..2774f9237e84 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf @@ -39,6 +39,11 @@ Nie znaleziono pakietu {0} w kanałach informacyjnych NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf index beea5f9cf625..018fb56a4b33 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf @@ -39,6 +39,11 @@ {0} não foi encontrado no NuGet feeds {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf index 870e9957d7b5..6d8ab98474ba 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf @@ -39,6 +39,11 @@ {0} не найдено в веб-каналах NuGet {1}. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf index 01b1300840a1..d90e0756b192 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf @@ -39,6 +39,11 @@ {0}, {1} NuGet akışlarında bulunamadı. + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf index 2dea90b8c612..578284141d94 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf @@ -39,6 +39,11 @@ 在 NuGet 源 {1} 中找不到 {0}。 + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf index 46a8c60a1b69..6f05220c3a7b 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf @@ -39,6 +39,11 @@ 在 NuGet 摘要 {1} 中找不到 {0}"。 + + Package {0} is not a .NET tool. + Package {0} is not a .NET tool. + + Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation. diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index 360e530e3b49..2bb88a290745 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -38,8 +38,8 @@ private static string GetCachedDeviceId() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Get device Id from Windows registry - using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + // Get device Id from Windows registry matching the OS architecture + using (var key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) { deviceId = key?.GetValue("deviceid") as string; } @@ -80,10 +80,16 @@ private static void CacheDeviceId(string deviceId) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Cache device Id in Windows registry - using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + // Cache device Id in Windows registry matching the OS architecture + using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) { - key.SetValue("deviceid", deviceId); + using(var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + { + if (key != null) + { + key.SetValue("deviceid", deviceId); + } + } } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs index 3189520dcf89..5173997199a0 100644 --- a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs +++ b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs @@ -130,13 +130,21 @@ public IToolPackage InstallPackage(PackageLocation packageLocation, PackageId pa { DownloadAndExtractPackage(packageId, nugetPackageDownloader, toolDownloadDir.Value, packageVersion, packageSourceLocation, includeUnlisted: givenSpecificVersion).GetAwaiter().GetResult(); } - else if(isGlobalTool) + else { - throw new ToolPackageException( - string.Format( - CommonLocalizableStrings.ToolPackageConflictPackageId, - packageId, - packageVersion.ToNormalizedString())); + if (!ToolPackageInstance.IsToolPackage(package.Nuspec.Xml)) + { + throw new GracefulException(string.Format(NuGetPackageDownloader.LocalizableStrings.NotATool, packageId)); + } + + if (isGlobalTool) + { + throw new ToolPackageException( + string.Format( + CommonLocalizableStrings.ToolPackageConflictPackageId, + packageId, + packageVersion.ToNormalizedString())); + } } CreateAssetFile(packageId, packageVersion, toolDownloadDir, assetFileDirectory, _runtimeJsonPath, targetFramework); @@ -277,7 +285,7 @@ private static async Task DownloadAndExtractPackage( bool includeUnlisted = false ) { - var packagePath = await nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation, includeUnlisted: includeUnlisted).ConfigureAwait(false); + var packagePath = await nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation, includeUnlisted: includeUnlisted, isTool: true).ConfigureAwait(false); // look for package on disk and read the version NuGetVersion version; diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs b/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs index 4bcd76b82bdc..b9299943926e 100644 --- a/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs +++ b/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs @@ -23,6 +23,17 @@ public static ToolPackageInstance CreateFromAssetFile(PackageId id, DirectoryPat return new ToolPackageInstance(id, version, packageDirectory, assetsJsonParentDirectory); } + + /// + /// Validates that the nuspec XML represents a .NET tool package. + /// + /// The nuspec XML to check. + /// if the nuspec represents a .NET tool package; otherwise, . + public static bool IsToolPackage(XDocument nuspec) => + nuspec.Root.Descendants().Where( + e => e.Name.LocalName == "packageType" && + e.Attributes().Where(a => a.Name.LocalName == "name" && a.Value == "DotnetTool").Any()).Any(); + private const string PackagedShimsDirectoryConvention = "shims"; public IEnumerable Warnings => _toolConfiguration.Value.Warnings; diff --git a/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs b/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs index 18dc4a50ce22..4f2af8f52567 100644 --- a/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs +++ b/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs @@ -18,6 +18,9 @@ public sealed class MSBuildLogger : INodeLogger internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval"; internal const string BuildTelemetryEventName = "build"; internal const string LoggingConfigurationTelemetryEventName = "loggingConfiguration"; + internal const string BuildcheckAcquisitionFailureEventName = "buildcheck/acquisitionfailure"; + internal const string BuildcheckRunEventName = "buildcheck/run"; + internal const string BuildcheckRuleStatsEventName = "buildcheck/rule"; internal const string SdkTaskBaseCatchExceptionTelemetryEventName = "taskBaseCatchException"; internal const string PublishPropertiesTelemetryEventName = "PublishProperties"; @@ -138,6 +141,21 @@ internal static void FormatAndSend(ITelemetry telemetry, TelemetryEventArgs args toBeHashed: Array.Empty(), toBeMeasured: new[] { "FileLoggersCount" }); break; + case BuildcheckAcquisitionFailureEventName: + TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties, + toBeHashed: new[] { "AssemblyName", "ExceptionType", "ExceptionMessage" }, + toBeMeasured: Array.Empty()); + break; + case BuildcheckRunEventName: + TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties, + toBeHashed: Array.Empty(), + toBeMeasured: new[] { "TotalRuntimeInMilliseconds" }); + break; + case BuildcheckRuleStatsEventName: + TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties, + toBeHashed: new[] { "RuleId", "CheckFriendlyName" }, + toBeMeasured: new[] { "TotalRuntimeInMilliseconds" }); + break; // Pass through events that don't need special handling case SdkTaskBaseCatchExceptionTelemetryEventName: case PublishPropertiesTelemetryEventName: diff --git a/src/Cli/dotnet/commands/dotnet-run/Program.cs b/src/Cli/dotnet/commands/dotnet-run/Program.cs index 8399465d5ced..cefc00ba84b6 100644 --- a/src/Cli/dotnet/commands/dotnet-run/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-run/Program.cs @@ -24,6 +24,35 @@ public static RunCommand FromParseResult(ParseResult parseResult) parseResult = ModifyParseResultForShorthandProjectOption(parseResult); } + // if the application arguments contain any binlog args then we need to remove them from the application arguments and apply + // them to the restore args. + // this is because we can't model the binlog command structure in MSbuild in the System.CommandLine parser, but we need + // bl information to synchronize the restore and build logger configurations + var applicationArguments = parseResult.GetValue(RunCommandParser.ApplicationArguments).ToList(); + + var binlogArgs = new List(); + var nonBinLogArgs = new List(); + foreach (var arg in applicationArguments) + { + + if (arg.StartsWith("/bl:") || arg.Equals("/bl") + || arg.StartsWith("--binaryLogger:") || arg.Equals("--binaryLogger") + || arg.StartsWith("-bl:") || arg.Equals("-bl")) + { + binlogArgs.Add(arg); + } + else + { + nonBinLogArgs.Add(arg); + } + } + + var restoreArgs = parseResult.OptionValuesToBeForwarded(RunCommandParser.GetCommand()).ToList(); + if (binlogArgs.Count > 0) + { + restoreArgs.AddRange(binlogArgs); + } + var command = new RunCommand( noBuild: parseResult.HasOption(RunCommandParser.NoBuildOption), projectFileOrDirectory: parseResult.GetValue(RunCommandParser.ProjectOption), @@ -32,8 +61,8 @@ public static RunCommand FromParseResult(ParseResult parseResult) noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption), interactive: parseResult.HasOption(RunCommandParser.InteractiveOption), verbosity: parseResult.HasOption(CommonOptions.VerbosityOption) ? parseResult.GetValue(CommonOptions.VerbosityOption) : null, - restoreArgs: parseResult.OptionValuesToBeForwarded(RunCommandParser.GetCommand()).ToArray(), - args: parseResult.GetValue(RunCommandParser.ApplicationArguments) + restoreArgs: restoreArgs.ToArray(), + args: nonBinLogArgs.ToArray() ); return command; diff --git a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs index e7b154267041..56e10007691a 100644 --- a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs @@ -4,6 +4,7 @@ #nullable enable using System.Reflection; +using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -225,15 +226,16 @@ private string[] GetRestoreArguments(IEnumerable cliRestoreArgs) private ICommand GetTargetCommand() { - // TODO for MSBuild usage here: need to sync loggers (primarily binlog) used with this evaluation - var project = EvaluateProject(ProjectFileFullPath, RestoreArgs); + FacadeLogger? logger = DetermineBinlogger(RestoreArgs); + var project = EvaluateProject(ProjectFileFullPath, RestoreArgs, logger); ValidatePreconditions(project); - InvokeRunArgumentsTarget(project, RestoreArgs, Verbosity); + InvokeRunArgumentsTarget(project, RestoreArgs, Verbosity, logger); + logger?.ReallyShutdown(); var runProperties = ReadRunPropertiesFromProject(project, Args); var command = CreateCommandFromRunProperties(project, runProperties); return command; - static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreArgs) + static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreArgs, ILogger? binaryLogger) { var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -251,8 +253,8 @@ static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreA globalProperties[key] = string.Join(";", values); } } - var project = new ProjectInstance(projectFilePath, globalProperties, null); - return project; + var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + return collection.LoadProject(projectFilePath).CreateProjectInstance(); } static void ValidatePreconditions(ProjectInstance project) @@ -329,34 +331,152 @@ static ICommand CreateCommandFromRunProperties(ProjectInstance project, RunPrope return command; } - static void InvokeRunArgumentsTarget(ProjectInstance project, string[] restoreArgs, VerbosityOptions? verbosity) + static void InvokeRunArgumentsTarget(ProjectInstance project, string[] restoreArgs, VerbosityOptions? verbosity, FacadeLogger? binaryLogger) { // if the restoreArgs contain a `-bl` then let's probe it List loggersForBuild = [ MakeTerminalLogger(verbosity) ]; - if (restoreArgs.FirstOrDefault(arg => arg.StartsWith("-bl", StringComparison.OrdinalIgnoreCase)) is string blArg) + if (binaryLogger is not null) + { + loggersForBuild.Add(binaryLogger); + } + + if (!project.Build([ComputeRunArgumentsTarget], loggers: loggersForBuild, remoteLoggers: null, out var _targetOutputs)) + { + throw new GracefulException(LocalizableStrings.RunCommandEvaluationExceptionBuildFailed, ComputeRunArgumentsTarget); + } + } + + static FacadeLogger? DetermineBinlogger(string[] restoreArgs) + { + + List binaryLoggers = new(); + + foreach (var blArg in restoreArgs.Where(arg => arg.StartsWith("-bl", StringComparison.OrdinalIgnoreCase))) { if (blArg.Contains(':')) { // split and forward args var split = blArg.Split(':', 2); - loggersForBuild.Add(new BinaryLogger { Parameters = split[1] }); + var filename = split[1]; + if (filename.EndsWith(".binlog")) + { + filename = filename.Substring(0, filename.Length - ".binlog".Length); + filename = filename + "-dotnet-run" + ".binlog"; + } + binaryLoggers.Add(new BinaryLogger { Parameters = filename }); } else { - // just the defaults - loggersForBuild.Add(new BinaryLogger { Parameters = "{}.binlog" }); + // the same name will be used for the build and run-restore-exec steps, so we need to make sure they don't conflict + var filename = "msbuild-dotnet-run" + ".binlog"; + binaryLoggers.Add(new BinaryLogger { Parameters = filename }); } - }; + } - if (!project.Build([ComputeRunArgumentsTarget], loggers: loggersForBuild, remoteLoggers: null, out var _targetOutputs)) + // this binaryLogger needs to be used for both evaluation and execution, so we need to only call it with a single IEventSource across + // both of those phases. + // We need a custom logger to handle this, because the MSBuild API for evaluation and execution calls logger Initialize and Shutdown methods, so will not allow us to do this. + if (binaryLoggers.Count > 0) { - throw new GracefulException(LocalizableStrings.RunCommandEvaluationExceptionBuildFailed, ComputeRunArgumentsTarget); + var fakeLogger = ConfigureDispatcher(binaryLoggers); + + return fakeLogger; + } + return null; + } + + static FacadeLogger ConfigureDispatcher(List binaryLoggers) + { + var dispatcher = new PersistentDispatcher(binaryLoggers); + return new FacadeLogger(dispatcher); + } + } + + /// + /// This class acts as a wrapper around the BinaryLogger, to allow us to keep the BinaryLogger alive across multiple phases of the build. + /// The methods here are stubs so that the real binarylogger sees that we support these functionalities. + /// We need to ensure that the child logger is Initialized and Shutdown only once, so this fake event source + /// acts as a buffer. We'll provide this dispatcher to another fake logger, and that logger will + /// bind to this dispatcher to foward events from the actual build to the binary logger through this dispatcher. + /// + /// + private class PersistentDispatcher : EventArgsDispatcher, IEventSource4 + { + private List innerLoggers; + + public PersistentDispatcher(List innerLoggers) + { + this.innerLoggers = innerLoggers; + foreach (var logger in innerLoggers) + { + logger.Initialize(this); + } + } + public event TelemetryEventHandler TelemetryLogged { add { } remove { } } + + public void IncludeEvaluationMetaprojects() { } + public void IncludeEvaluationProfiles() { } + public void IncludeEvaluationPropertiesAndItems() { } + public void IncludeTaskInputs() { } + + public void Destroy() + { + foreach (var innerLogger in innerLoggers) + { + innerLogger.Shutdown(); } } } + /// + /// This logger acts as a forwarder to the provided dispatcher, so that multiple different build engine operations + /// can be forwarded to the shared binary logger held by the dispatcher. + /// We opt into lots of data to ensure that we can forward all events to the binary logger. + /// + /// + private class FacadeLogger(PersistentDispatcher dispatcher) : ILogger + { + public PersistentDispatcher Dispatcher => dispatcher; + + public LoggerVerbosity Verbosity { get => LoggerVerbosity.Diagnostic; set { } } + public string? Parameters { get => ""; set { } } + + public void Initialize(IEventSource eventSource) + { + if (eventSource is IEventSource3 eventSource3) + { + eventSource3.IncludeEvaluationMetaprojects(); + dispatcher.IncludeEvaluationMetaprojects(); + + eventSource3.IncludeEvaluationProfiles(); + dispatcher.IncludeEvaluationProfiles(); + + eventSource3.IncludeTaskInputs(); + dispatcher.IncludeTaskInputs(); + } + + eventSource.AnyEventRaised += (sender, args) => dispatcher.Dispatch(args); + + if (eventSource is IEventSource4 eventSource4) + { + eventSource4.IncludeEvaluationPropertiesAndItems(); + dispatcher.IncludeEvaluationPropertiesAndItems(); + } + } + + public void ReallyShutdown() + { + dispatcher.Destroy(); + } + + // we don't do anything on shutdown, because we want to keep the dispatcher alive for the next phase + public void Shutdown() + { + } + } + static ILogger MakeTerminalLogger(VerbosityOptions? verbosity) { var msbuildVerbosity = ToLoggerVerbosity(verbosity); diff --git a/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs b/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs index 1499142be4c0..42b9dd56417e 100644 --- a/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs @@ -8,6 +8,7 @@ using Microsoft.DotNet.ToolManifest; using Microsoft.DotNet.ToolPackage; using Microsoft.DotNet.Tools.Tool.Common; +using Microsoft.Extensions.EnvironmentAbstractions; using CreateShellShimRepository = Microsoft.DotNet.Tools.Tool.Install.CreateShellShimRepository; namespace Microsoft.DotNet.Tools.Tool.Update @@ -43,16 +44,17 @@ public ToolUpdateCommand( localToolsResolverCache, reporter); + _global = result.GetValue(ToolInstallCommandParser.GlobalOption); + _toolPath = result.GetValue(ToolInstallCommandParser.ToolPathOption); + DirectoryPath? location = string.IsNullOrWhiteSpace(_toolPath) ? null : new DirectoryPath(_toolPath); _toolUpdateGlobalOrToolPathCommand = toolUpdateGlobalOrToolPathCommand ?? new ToolUpdateGlobalOrToolPathCommand( result, createToolPackageStoreDownloaderUninstaller, createShellShimRepository, - reporter); - - _global = result.GetValue(ToolInstallCommandParser.GlobalOption); - _toolPath = result.GetValue(ToolInstallCommandParser.ToolPathOption); + reporter, + ToolPackageFactory.CreateToolPackageStoreQuery(location)); } diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs index dc36affa9eda..f50e0551e9ca 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs @@ -39,7 +39,10 @@ internal static string GetWorkloadsVersion(WorkloadInfoHelper workloadInfoHelper { workloadInfoHelper ??= new WorkloadInfoHelper(false); - return workloadInfoHelper.ManifestProvider.GetWorkloadVersion().Version; + var versionInfo = workloadInfoHelper.ManifestProvider.GetWorkloadVersion(); + + // The explicit space here is intentional, as it's easy to miss in localization and crucial for parsing + return versionInfo.Version + (versionInfo.VersionNotInstalledMessage is not null ? ' ' + Workloads.Workload.List.LocalizableStrings.WorkloadVersionNotInstalledShort : string.Empty); } internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null, bool showVersion = true) @@ -57,46 +60,68 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadI InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList); string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); + + + var versionInfo = workloadInfoHelper.ManifestProvider.GetWorkloadVersion(); + + void WriteUpdateModeAndAnyError(string indent = "") + { + var useWorkloadSets = InstallStateContents.FromPath(Path.Combine(WorkloadInstallType.GetInstallStateFolder(workloadInfoHelper._currentSdkFeatureBand, workloadInfoHelper.UserLocalPath), "default.json")).UseWorkloadSets; + var workloadSetsString = useWorkloadSets == true ? "workload sets" : "loose manifests"; + reporter.WriteLine(indent + string.Format(CommonStrings.WorkloadManifestInstallationConfiguration, workloadSetsString)); + + var additionalMessage = versionInfo.VersionNotInstalledMessage ?? versionInfo.UpdateModeMessage; + if (additionalMessage != null) + { + reporter.WriteLine(indent + additionalMessage); + } + } + if (showVersion) { - reporter.WriteLine($" Workload version: {workloadInfoHelper.ManifestProvider.GetWorkloadVersion().Version}"); + reporter.WriteLine($" Workload version: {GetWorkloadsVersion()}"); + + WriteUpdateModeAndAnyError(indent: " "); + reporter.WriteLine(); } - var useWorkloadSets = InstallStateContents.FromPath(Path.Combine(WorkloadInstallType.GetInstallStateFolder(workloadInfoHelper._currentSdkFeatureBand, workloadInfoHelper.UserLocalPath), "default.json")).UseWorkloadSets; - var workloadSetsString = useWorkloadSets == true ? "workload sets" : "loose manifests"; - reporter.WriteLine(string.Format(CommonStrings.WorkloadManifestInstallationConfiguration, workloadSetsString)); - if (installedWorkloads.Count == 0) { reporter.WriteLine(CommonStrings.NoWorkloadsInstalledInfoWarning); - return; } + else + { + var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase); - var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase); + foreach (var workload in installedWorkloads.AsEnumerable()) + { + var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workload.Key)); + var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand; - foreach (var workload in installedWorkloads.AsEnumerable()) - { - var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workload.Key)); - var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand; + const int align = 10; + const string separator = " "; - const int align = 10; - const string separator = " "; + reporter.WriteLine($" [{workload.Key}]"); - reporter.WriteLine($" [{workload.Key}]"); + reporter.Write($"{separator}{CommonStrings.WorkloadSourceColumn}:"); + reporter.WriteLine($" {workload.Value,align}"); - reporter.Write($"{separator}{CommonStrings.WorkloadSourceColumn}:"); - reporter.WriteLine($" {workload.Value,align}"); + reporter.Write($"{separator}{CommonStrings.WorkloadManifestVersionColumn}:"); + reporter.WriteLine($" {workloadManifest.Version + '/' + workloadFeatureBand,align}"); - reporter.Write($"{separator}{CommonStrings.WorkloadManifestVersionColumn}:"); - reporter.WriteLine($" {workloadManifest.Version + '/' + workloadFeatureBand,align}"); + reporter.Write($"{separator}{CommonStrings.WorkloadManifestPathColumn}:"); + reporter.WriteLine($" {workloadManifest.ManifestPath,align}"); - reporter.Write($"{separator}{CommonStrings.WorkloadManifestPathColumn}:"); - reporter.WriteLine($" {workloadManifest.ManifestPath,align}"); + reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:"); + reporter.WriteLine($" {WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(Utils.Product.Version), dotnetPath).ToString(),align}" + ); + reporter.WriteLine(""); + } + } - reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:"); - reporter.WriteLine($" {WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(Utils.Product.Version), dotnetPath).ToString(),align}" - ); - reporter.WriteLine(""); + if (!showVersion) + { + WriteUpdateModeAndAnyError(); } } diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs index ac0e1e358a36..270284bce9b4 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs @@ -54,8 +54,7 @@ public void Run(Action workloadAction) private WorkloadHistoryState GetWorkloadState() { var resolver = _workloadResolverFunc(); - var currentWorkloadVersion = resolver.GetWorkloadVersion(); - var versionString = currentWorkloadVersion.WorkloadInstallType == WorkloadVersion.Type.LooseManifest ? string.Empty : currentWorkloadVersion.Version; + var currentWorkloadVersion = resolver.GetWorkloadVersion().Version; return new WorkloadHistoryState() { ManifestVersions = resolver.GetInstalledManifests().ToDictionary(manifest => manifest.Id.ToString(), manifest => $"{manifest.Version}/{manifest.ManifestFeatureBand}"), @@ -63,9 +62,9 @@ private WorkloadHistoryState GetWorkloadState() .GetInstalledWorkloads(new SdkFeatureBand(_workloadResolver.GetSdkFeatureBand())) .Select(id => id.ToString()) .ToList(), - WorkloadSetVersion = versionString + WorkloadSetVersion = currentWorkloadVersion }; } } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs index 0ff7756e7637..d709227d2292 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs @@ -48,7 +48,8 @@ public FileBasedInstaller(IReporter reporter, string tempDirPath = null, VerbosityOptions verbosity = VerbosityOptions.normal, PackageSourceLocation packageSourceLocation = null, - RestoreActionConfig restoreActionConfig = null) + RestoreActionConfig restoreActionConfig = null, + VerbosityOptions nugetPackageDownloaderVerbosity = VerbosityOptions.normal) { _userProfileDir = userProfileDir; _dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); @@ -58,7 +59,8 @@ public FileBasedInstaller(IReporter reporter, _nugetPackageDownloader = nugetPackageDownloader ?? new NuGetPackageDownloader(_tempPackagesDir, filePermissionSetter: null, new FirstPartyNuGetPackageSigningVerifier(), logger, - restoreActionConfig: _restoreActionConfig); + restoreActionConfig: _restoreActionConfig, + verbosityOptions: nugetPackageDownloaderVerbosity); bool userLocal = WorkloadFileBasedInstall.IsUserLocal(_dotnetDir, sdkFeatureBand.ToString()); _workloadRootDir = userLocal ? _userProfileDir : _dotnetDir; _workloadMetadataDir = Path.Combine(_workloadRootDir, "metadata", "workloads"); @@ -92,9 +94,8 @@ IEnumerable GetPacksInWorkloads(IEnumerable workloadIds) public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) { - SdkFeatureBand workloadSetFeatureBand; - string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out workloadSetFeatureBand); - var workloadSetPackageId = GetManifestPackageId(new ManifestId("Microsoft.NET.Workloads"), workloadSetFeatureBand); + string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out SdkFeatureBand workloadSetFeatureBand); + var workloadSetPackageId = GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand); var workloadSetPath = Path.Combine(_workloadRootDir, "sdk-manifests", _sdkFeatureBand.ToString(), "workloadsets", workloadSetVersion); @@ -119,6 +120,18 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo return WorkloadSet.FromWorkloadSetFolder(workloadSetPath, workloadSetVersion, _sdkFeatureBand); } + public WorkloadSet GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult(); + + public async Task GetWorkloadSetContentsAsync(string workloadSetVersion) + { + string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand); + var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand), + new NuGetVersion(workloadSetPackageVersion), _packageSourceLocation); + var tempExtractionDir = Path.Combine(_tempPackagesDir.Value, $"{WorkloadManifestUpdater.WorkloadSetManifestId}-{workloadSetPackageVersion}-extracted"); + await ExtractManifestAsync(packagePath, tempExtractionDir); + return WorkloadSet.FromWorkloadSetFolder(tempExtractionDir, workloadSetVersion, _sdkFeatureBand); + } + public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null) { var packInfos = GetPacksInWorkloads(workloadIds); diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs index a80755c6af41..3f3e984d6eac 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs @@ -20,6 +20,7 @@ internal class WorkloadInstallCommand : InstallingWorkloadCommand { private bool _skipManifestUpdate; private readonly IReadOnlyCollection _workloadIds; + private readonly bool _shouldShutdownInstaller; public bool IsRunningRestore { get; set; } @@ -45,6 +46,7 @@ public WorkloadInstallCommand( WorkloadInstallerFactory.GetWorkloadInstaller(resolvedReporter, _sdkFeatureBand, _workloadResolver, Verbosity, _userProfileDir, VerifySignatures, PackageDownloader, _dotnetPath, TempDirectoryPath, _packageSourceLocation, RestoreActionConfiguration, elevationRequired: !_printDownloadLinkOnly && string.IsNullOrWhiteSpace(_downloadToCacheOption)); + _shouldShutdownInstaller = _workloadInstallerFromConstructor != null; _workloadManifestUpdater = _workloadManifestUpdaterFromConstructor ?? new WorkloadManifestUpdater(resolvedReporter, _workloadResolver, PackageDownloader, _userProfileDir, _workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, displayManifestUpdates: Verbosity.IsDetailedOrDiagnostic()); @@ -120,7 +122,9 @@ public override int Execute() throw new GracefulException(string.Format(LocalizableStrings.CannotCombineSkipManifestAndVersion, WorkloadInstallCommandParser.SkipManifestUpdateOption.Name, InstallingWorkloadCommandParser.VersionOption.Name), isUserError: true); } - else if (_skipManifestUpdate && SpecifiedWorkloadSetVersionInGlobalJson) + else if ((_skipManifestUpdate && SpecifiedWorkloadSetVersionInGlobalJson) && + !IsRunningRestore) // When running restore, we first update workloads, then query the projects to figure out what workloads should be installed, then run the install command. + // When we run the install command we set skipManifestUpdate to true as an optimization to avoid trying to update twice { throw new GracefulException(string.Format(LocalizableStrings.CannotUseSkipManifestWithGlobalJsonWorkloadVersion, WorkloadInstallCommandParser.SkipManifestUpdateOption.Name, _globalJsonPath), isUserError: true); @@ -146,14 +150,21 @@ public override int Execute() } catch (Exception e) { - _workloadInstaller.Shutdown(); + if (_shouldShutdownInstaller) + { + _workloadInstaller.Shutdown(); + } // Don't show entire stack trace throw new GracefulException(string.Format(LocalizableStrings.WorkloadInstallationFailed, e.Message), e, isUserError: false); } } - _workloadInstaller.Shutdown(); + if (_shouldShutdownInstaller) + { + _workloadInstaller.Shutdown(); + } + return _workloadInstaller.ExitCode; } diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs index b0e420496375..5f86698b72df 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs @@ -20,7 +20,7 @@ namespace Microsoft.DotNet.Workloads.Workload.Install { internal class WorkloadManifestUpdater : IWorkloadManifestUpdater { - readonly string WorkloadSetManifestId = "Microsoft.NET.Workloads"; + public static readonly string WorkloadSetManifestId = "Microsoft.NET.Workloads"; private readonly IReporter _reporter; private readonly IWorkloadResolver _workloadResolver; diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx index 4ba629fee8d5..e82e8b1e6a75 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx @@ -134,6 +134,9 @@ Found workload version {0} pinned in the global.json file at {1}. + + (not installed) + Found workload version {0} pinned in the global.json file at {1}, but it was not installed. Running `dotnet workload install`, `dotnet workload update`, or `dotnet workload restore` may fix this. diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf index 722830ef8eb3..2a900b4f2dc2 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf @@ -32,6 +32,11 @@ Aktualizace jsou k dispozici pro následující úlohy: {0}. Pokud chcete získat nejnovější verzi, spusťte aktualizaci úlohy dotnet (`dotnet workload update`). {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.de.xlf index cc37a79c6050..6d8a6ad9375a 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.de.xlf @@ -32,6 +32,11 @@ Updates sind für die folgenden Workloads verfügbar: {0}. Führen Sie „dotnet workload update“ aus, um die neueste Updates zu erhalten. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.es.xlf index 0144e50a38cb..6fccba23b95c 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.es.xlf @@ -32,6 +32,11 @@ Hay actualizaciones disponibles para las siguientes cargas de trabajo: {0}. Ejecute "dotnet workload update" para obtener la versión más reciente. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.fr.xlf index 651a34096ff9..fe1b3f8895a5 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.fr.xlf @@ -32,6 +32,11 @@ Des mises à jour sont disponibles pour les charges de travail suivantes : {0}. Exécutez `dotnet workload update` pour obtenir la dernière version. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.it.xlf index 40e37f31dbe1..0a3bb9ad6c81 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.it.xlf @@ -32,6 +32,11 @@ Gli aggiornamenti sono disponibili per i carichi di lavoro seguenti: {0}. Per ottenere la versione più recente, eseguire 'dotnet workload update'. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ja.xlf index 906a1e304014..b0a53e52f449 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ja.xlf @@ -32,6 +32,11 @@ 次のワークロードについて更新プログラムを入手可能です: {0}。最新版を取得するには、`dotnet workload update` を実行します。 {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ko.xlf index e3a03464b07a..41b8060ef14e 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ko.xlf @@ -32,6 +32,11 @@ 다음 워크로드에 대한 업데이트를 사용할 수 있습니다. {0}. 최신 버전을 받으려면 `dotnet workload update`를 실행하세요. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pl.xlf index c925df5fcd75..125782bec3e1 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pl.xlf @@ -32,6 +32,11 @@ Aktualizacje są dostępne dla następujących obciążeń: {0}. Uruchom polecenie `dotnet workload update`, aby uzyskać najnowszą wersję. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pt-BR.xlf index d815267266d6..29bffc61132f 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.pt-BR.xlf @@ -32,6 +32,11 @@ As atualizações estão disponíveis para as seguintes cargas de trabalho(s): {0}. Execute `dotnet workload update` para obter o mais recente. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ru.xlf index 55c0c4924564..8f2c10c716f0 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.ru.xlf @@ -32,6 +32,11 @@ Обновления доступны для следующих рабочих нагрузок: {0}. Чтобы получить последнюю версию, запустите `dotnet workload update`. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.tr.xlf index e2fd1f6d4bbb..b60e77b79c1a 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.tr.xlf @@ -32,6 +32,11 @@ Şu iş yükleri için güncelleştirmeler var: {0}. En son sürümü almak için `dotnet workload update` çalıştırın. {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hans.xlf index 110a836e90c0..5cffb456a7de 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hans.xlf @@ -32,6 +32,11 @@ 以下工作负载有可用的更新: {0}。请运行 `dotnet workload update` 以获取最新版本。 {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hant.xlf index c3838d6dd072..2d36f2b1639f 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.zh-Hant.xlf @@ -32,6 +32,11 @@ 以下工作負載有可用的更新: {0}。執行 `dotnet workload update` 以取得最新更新。 {Locked="dotnet workload update"} + + (not installed) + (not installed) + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/restore/WorkloadRestoreCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/restore/WorkloadRestoreCommand.cs index d02a72c2d6d6..94c40d95dc4c 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/restore/WorkloadRestoreCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/restore/WorkloadRestoreCommand.cs @@ -51,7 +51,7 @@ public override int Execute() recorder.Run(() => { // First update manifests and install a workload set as necessary - new WorkloadUpdateCommand(_result, recorder: recorder).Execute(); + new WorkloadUpdateCommand(_result, recorder: recorder, isRestoring: true).Execute(); var allProjects = DiscoverAllProjects(Directory.GetCurrentDirectory(), _slnOrProjectArgument).Distinct(); List allWorkloadId = RunTargetToGetWorkloadIds(allProjects); @@ -64,6 +64,8 @@ public override int Execute() IsRunningRestore = true }.Execute(); }); + + workloadInstaller.Shutdown(); return 0; } diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-workload/search/LocalizableStrings.resx index fd9f416abf30..cce5241a328e 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-workload/search/LocalizableStrings.resx @@ -145,4 +145,23 @@ The --take option must be positive. + + WORKLOAD_VERSION + + + Output workload manifest versions associated with the provided workload version. + + + Cannot specify both the {0} and {1} arguments. + + + No workload versions found for SDK feature band {0}. + + + Workload manifest ID + + + Manifest feature band + + diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommand.cs index 3cbe4e79dbab..05bee83802e2 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommand.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using System.Text.Json; -using Microsoft.Deployment.DotNet.Releases; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; @@ -16,12 +14,7 @@ namespace Microsoft.DotNet.Workloads.Workload.Search internal class WorkloadSearchCommand : WorkloadCommandBase { private readonly IWorkloadResolver _workloadResolver; - private readonly ReleaseVersion _sdkVersion; private readonly string _workloadIdStub; - private readonly int _numberOfWorkloadSetsToTake; - private readonly string _workloadSetOutputFormat; - private readonly IWorkloadManifestInstaller _installer; - internal bool ListWorkloadSetVersions { get; set; } = false; public WorkloadSearchCommand( ParseResult result, @@ -30,7 +23,7 @@ public WorkloadSearchCommand( { _workloadIdStub = result.GetValue(WorkloadSearchCommandParser.WorkloadIdStubArgument); - workloadResolverFactory = workloadResolverFactory ?? new WorkloadResolverFactory(); + workloadResolverFactory ??= new WorkloadResolverFactory(); if (!string.IsNullOrEmpty(result.GetValue(WorkloadSearchCommandParser.VersionOption))) { @@ -39,45 +32,11 @@ public WorkloadSearchCommand( var creationResult = workloadResolverFactory.Create(); - _sdkVersion = creationResult.SdkVersion; _workloadResolver = creationResult.WorkloadResolver; - - _numberOfWorkloadSetsToTake = result.GetValue(SearchWorkloadSetsParser.TakeOption); - _workloadSetOutputFormat = result.GetValue(SearchWorkloadSetsParser.FormatOption); - - _installer = WorkloadInstallerFactory.GetWorkloadInstaller( - reporter, - new SdkFeatureBand(_sdkVersion), - _workloadResolver, - Verbosity, - creationResult.UserProfileDir, - !SignCheck.IsDotNetSigned(), - restoreActionConfig: new RestoreActionConfig(result.HasOption(SharedOptions.InteractiveOption)), - elevationRequired: false, - shouldLog: false); } public override int Execute() { - if (ListWorkloadSetVersions) - { - var featureBand = new SdkFeatureBand(_sdkVersion); - var packageId = _installer.GetManifestPackageId(new ManifestId("Microsoft.NET.Workloads"), featureBand); - var versions = PackageDownloader.GetLatestPackageVersions(packageId, _numberOfWorkloadSetsToTake, packageSourceLocation: null, includePreview: !string.IsNullOrWhiteSpace(_sdkVersion.Prerelease)) - .GetAwaiter().GetResult() - .Select(version => WorkloadManifestUpdater.WorkloadSetPackageVersionToWorkloadSetVersion(featureBand, version.Version.ToString())); - if (_workloadSetOutputFormat?.Equals("json", StringComparison.OrdinalIgnoreCase) == true) - { - Reporter.WriteLine(JsonSerializer.Serialize(versions.Select(version => version.ToDictionary(_ => "workloadVersion", v => v)))); - } - else - { - Reporter.WriteLine(string.Join('\n', versions)); - } - - return 0; - } - IEnumerable availableWorkloads = _workloadResolver.GetAvailableWorkloads() .OrderBy(workload => workload.Id); diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommandParser.cs index d4573cb168b9..3c4cc66b5391 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommandParser.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchCommandParser.cs @@ -29,7 +29,7 @@ public static CliCommand GetCommand() private static CliCommand ConstructCommand() { var command = new CliCommand("search", LocalizableStrings.CommandDescription); - command.Subcommands.Add(SearchWorkloadSetsParser.GetCommand()); + command.Subcommands.Add(WorkloadSearchVersionsCommandParser.GetCommand()); command.Arguments.Add(WorkloadIdStubArgument); command.Options.Add(CommonOptions.HiddenVerbosityOption); command.Options.Add(VersionOption); diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommand.cs new file mode 100644 index 000000000000..2e053530a52e --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommand.cs @@ -0,0 +1,122 @@ +// 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.CommandLine; +using System.Text.Json; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; +using Microsoft.DotNet.ToolPackage; +using Microsoft.DotNet.Workloads.Workload.Install; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using Microsoft.TemplateEngine.Cli.Commands; +using NuGet.Versioning; + +using InformationStrings = Microsoft.DotNet.Workloads.Workload.LocalizableStrings; + +namespace Microsoft.DotNet.Workloads.Workload.Search +{ + internal class WorkloadSearchVersionsCommand : WorkloadCommandBase + { + private readonly ReleaseVersion _sdkVersion; + private readonly int _numberOfWorkloadSetsToTake; + private readonly string _workloadSetOutputFormat; + private readonly FileBasedInstaller _installer; + private readonly string _workloadVersion; + + public WorkloadSearchVersionsCommand( + ParseResult result, + IReporter reporter = null, + IWorkloadResolverFactory workloadResolverFactory = null) : base(result, CommonOptions.HiddenVerbosityOption, reporter) + { + workloadResolverFactory = workloadResolverFactory ?? new WorkloadResolverFactory(); + + if (!string.IsNullOrEmpty(result.GetValue(WorkloadSearchCommandParser.VersionOption))) + { + throw new GracefulException(Install.LocalizableStrings.SdkVersionOptionNotSupported); + } + + var creationResult = workloadResolverFactory.Create(); + + _sdkVersion = creationResult.SdkVersion; + var workloadResolver = creationResult.WorkloadResolver; + + _numberOfWorkloadSetsToTake = result.GetValue(WorkloadSearchVersionsCommandParser.TakeOption); + _workloadSetOutputFormat = result.GetValue(WorkloadSearchVersionsCommandParser.FormatOption); + + // For these operations, we don't have to respect 'msi' because they're equivalent between the two workload + // install types, and FileBased is much easier to work with. + _installer = new FileBasedInstaller( + reporter, + new SdkFeatureBand(_sdkVersion), + workloadResolver, + CliFolderPathCalculator.DotnetUserProfileFolderPath, + nugetPackageDownloader: null, + dotnetDir: Path.GetDirectoryName(Environment.ProcessPath), + tempDirPath: null, + verbosity: Verbosity, + packageSourceLocation: null, + restoreActionConfig: new RestoreActionConfig(result.HasOption(SharedOptions.InteractiveOption)), + nugetPackageDownloaderVerbosity: VerbosityOptions.quiet + ); + + _workloadVersion = result.GetValue(WorkloadSearchVersionsCommandParser.WorkloadVersionArgument); + } + + public override int Execute() + { + if (_workloadVersion is null) + { + var featureBand = new SdkFeatureBand(_sdkVersion); + var packageId = _installer.GetManifestPackageId(new ManifestId("Microsoft.NET.Workloads"), featureBand); + + List versions; + try + { + versions = PackageDownloader.GetLatestPackageVersions(packageId, _numberOfWorkloadSetsToTake, packageSourceLocation: null, includePreview: !string.IsNullOrWhiteSpace(_sdkVersion.Prerelease)) + .GetAwaiter().GetResult() + .Select(version => WorkloadManifestUpdater.WorkloadSetPackageVersionToWorkloadSetVersion(featureBand, version.ToString())) + .ToList(); + } + catch (NuGetPackageNotFoundException) + { + Microsoft.DotNet.Cli.Utils.Reporter.Error.WriteLine(string.Format(LocalizableStrings.NoWorkloadVersionsFound, featureBand)); + return 0; + } + if (_workloadSetOutputFormat?.Equals("json", StringComparison.OrdinalIgnoreCase) == true) + { + Reporter.WriteLine(JsonSerializer.Serialize(versions.Select(version => version.ToDictionary(_ => "workloadVersion", v => v)))); + } + else + { + Reporter.WriteLine(string.Join('\n', versions)); + } + } + else + { + var workloadSet = _installer.GetWorkloadSetContents(_workloadVersion); + if (_workloadSetOutputFormat?.Equals("json", StringComparison.OrdinalIgnoreCase) == true) + { + var set = new WorkloadSet() { ManifestVersions = workloadSet.ManifestVersions }; + Reporter.WriteLine(JsonSerializer.Serialize(new Dictionary>() + { + { "manifestVersions", set.ToDictionaryForJson() } + }, new JsonSerializerOptions { WriteIndented = true })); + } + else + { + PrintableTable> table = new(); + table.AddColumn(LocalizableStrings.WorkloadManifestIdColumn, manifest => manifest.Key.ToString()); + table.AddColumn(LocalizableStrings.WorkloadManifestFeatureBandColumn, manifest => manifest.Value.FeatureBand.ToString()); + table.AddColumn(InformationStrings.WorkloadManifestVersionColumn, manifest => manifest.Value.Version.ToString()); + table.PrintRows(workloadSet.ManifestVersions, l => Reporter.WriteLine(l)); + } + } + + return 0; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/SearchWorkloadSetsParser.cs b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommandParser.cs similarity index 56% rename from src/Cli/dotnet/commands/dotnet-workload/search/SearchWorkloadSetsParser.cs rename to src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommandParser.cs index 5ffecb96ad9f..d40d53193457 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/SearchWorkloadSetsParser.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/search/WorkloadSearchVersionsCommandParser.cs @@ -7,8 +7,15 @@ namespace Microsoft.DotNet.Cli { - internal static class SearchWorkloadSetsParser + internal static class WorkloadSearchVersionsCommandParser { + public static readonly CliArgument WorkloadVersionArgument = + new(LocalizableStrings.WorkloadVersionArgument) + { + Arity = ArgumentArity.ZeroOrOne, + Description = LocalizableStrings.WorkloadVersionArgumentDescription + }; + public static readonly CliOption TakeOption = new("--take") { DefaultValueFactory = (_) => 5 }; public static readonly CliOption FormatOption = new("--format") @@ -26,6 +33,7 @@ public static CliCommand GetCommand() private static CliCommand ConstructCommand() { var command = new CliCommand("version", LocalizableStrings.PrintSetVersionsDescription); + command.Arguments.Add(WorkloadVersionArgument); command.Options.Add(FormatOption); command.Options.Add(TakeOption); @@ -33,14 +41,19 @@ private static CliCommand ConstructCommand() { if (optionResult.GetValueOrDefault() <= 0) { - throw new ArgumentException(LocalizableStrings.TakeOptionMustBePositive); + throw new ArgumentException("The --take option must be positive."); } }); - command.SetAction(parseResult => new WorkloadSearchCommand(parseResult) + command.Validators.Add(result => { - ListWorkloadSetVersions = true - }.Execute()); + if (result.GetValue(WorkloadSearchCommandParser.WorkloadIdStubArgument) != null) + { + result.AddError(string.Format(LocalizableStrings.CannotCombineSearchStringAndVersion, WorkloadSearchCommandParser.WorkloadIdStubArgument.Name, command.Name)); + } + }); + + command.SetAction(parseResult => new WorkloadSearchVersionsCommand(parseResult).Execute()); return command; } diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.cs.xlf index 4a242ec47e04..bd5410311008 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.cs.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Vyhledávejte dostupné úlohy. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Platformy @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.de.xlf index 8e6f44d5c98f..f0546411d276 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.de.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Suchen Sie nach verfügbaren Workloads. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Plattformen @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.es.xlf index 5dcfe83d51ca..22ea37b9f3f7 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.es.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Permite buscar cargas de trabajo disponibles. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Plataformas @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.fr.xlf index 9cdb89c94765..95957071a2a1 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.fr.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Recherchez les charges de travail disponibles. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Plateformes @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.it.xlf index 74f8259e7a7d..27c32bc8dacb 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.it.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Cercare carichi di lavoro disponibili. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Piattaforme @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ja.xlf index 114e086d1741..92457c8099d1 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ja.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. 使用可能なワークロードを検索します。 @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms プラットフォーム @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ko.xlf index 0b176603098e..c4bda2224fac 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ko.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. 사용 가능한 워크로드를 검색합니다. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms 플랫폼 @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pl.xlf index 3973c8445fdc..9f2d67278c18 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pl.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Wyszukaj dostępne obciążenia. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Platformy @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pt-BR.xlf index ec59bb07dea8..89ec96982510 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.pt-BR.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Pesquise cargas de trabalho disponíveis. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Plataformas @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ru.xlf index 6c690d53352d..6bcf7c2e23e2 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.ru.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Поиск доступных рабочих нагрузок. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Платформы @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.tr.xlf index d9b26a9b0ffb..e051510f5301 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.tr.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. Kullanılabilir iş yüklerini arayın. @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms Platformlar @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hans.xlf index 0f6ee207129c..abad799cc52b 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hans.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. 搜索可用的工作负载。 @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms 平台 @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hant.xlf index 0156ca1b3d1e..40833effe02c 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/search/xlf/LocalizableStrings.zh-Hant.xlf @@ -2,6 +2,11 @@ + + Cannot specify both the {0} and {1} arguments. + Cannot specify both the {0} and {1} arguments. + + Search for available workloads. 搜尋可用的工作負載。 @@ -17,6 +22,11 @@ Changes the format of outputted workload versions. Can take 'json' or 'list' + + No workload versions found for SDK feature band {0}. + No workload versions found for SDK feature band {0}. + + Platforms 平台 @@ -47,6 +57,26 @@ SEARCH_STRING + + Manifest feature band + Manifest feature band + + + + Workload manifest ID + Workload manifest ID + + + + WORKLOAD_VERSION + WORKLOAD_VERSION + + + + Output workload manifest versions associated with the provided workload version. + Output workload manifest versions associated with the provided workload version. + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/update/WorkloadUpdateCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/update/WorkloadUpdateCommand.cs index a403f5266329..4ecc1e10993f 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/update/WorkloadUpdateCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/update/WorkloadUpdateCommand.cs @@ -23,6 +23,7 @@ internal class WorkloadUpdateCommand : InstallingWorkloadCommand private readonly bool _fromPreviousSdk; private WorkloadHistoryRecorder _recorder; private readonly bool _isRestoring; + private readonly bool _shouldShutdownInstaller; public WorkloadUpdateCommand( ParseResult parseResult, IReporter reporter = null, @@ -49,6 +50,9 @@ public WorkloadUpdateCommand( _dotnetPath, TempDirectoryPath, packageSourceLocation: _packageSourceLocation, RestoreActionConfiguration, elevationRequired: !_printDownloadLinkOnly && !_printRollbackDefinitionOnly && string.IsNullOrWhiteSpace(_downloadToCacheOption)); + _shouldShutdownInstaller = _workloadInstallerFromConstructor != null; + + _workloadManifestUpdater = _workloadManifestUpdaterFromConstructor ?? new WorkloadManifestUpdater(resolvedReporter, _workloadResolver, PackageDownloader, _userProfileDir, _workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, sdkFeatureBand: _sdkFeatureBand); _recorder = recorder; @@ -132,7 +136,10 @@ public override int Execute() } } - _workloadInstaller.Shutdown(); + if (_shouldShutdownInstaller) + { + _workloadInstaller.Shutdown(); + } return _workloadInstaller.ExitCode; } diff --git a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets index 60075928f18e..3414f6e42983 100644 --- a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets +++ b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets @@ -44,18 +44,30 @@ Copyright (c) .NET Foundation. All rights reserved. - - - <_TargetingNETCoreApp30OrLater>true - <_TargetingNET50OrLater>true - <_TargetingNET60OrLater>true - <_TargetingNET70OrLater>true - <_TargetingNET80OrLater>true - true - 8.0 - - - + + + <_TargetingNETCoreApp30OrLater>true + <_TargetingNET50OrLater>true + <_TargetingNET60OrLater>true + <_TargetingNET70OrLater>true + <_TargetingNET80OrLater>true + <_TargetingNET90OrLater>true + true + 9.0 + + + + + <_TargetingNETCoreApp30OrLater>true + <_TargetingNET50OrLater>true + <_TargetingNET60OrLater>true + <_TargetingNET70OrLater>true + <_TargetingNET80OrLater>true + true + 8.0 + + + <_TargetingNETCoreApp30OrLater>true <_TargetingNET50OrLater>true diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs index 6c32db8e426f..c859a19355e7 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs @@ -14,9 +14,11 @@ public interface IWorkloadManifestProvider string GetSdkFeatureBand(); - WorkloadVersion GetWorkloadVersion(); + WorkloadVersionInfo GetWorkloadVersion(); Dictionary GetAvailableWorkloadSets(); + + public readonly record struct WorkloadVersionInfo(string Version, string? VersionNotInstalledMessage = null, string? UpdateModeMessage = null); } public record WorkloadVersion diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadResolver.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadResolver.cs index 4927ff82ecb7..9caf5257f4a9 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadResolver.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadResolver.cs @@ -20,7 +20,7 @@ public interface IWorkloadResolver string GetManifestFeatureBand(string manifestId); IEnumerable GetInstalledManifests(); string GetSdkFeatureBand(); - WorkloadVersion GetWorkloadVersion(); + IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion(); IEnumerable GetUpdatedWorkloads(WorkloadResolver advertisingManifestResolver, IEnumerable installedWorkloads); WorkloadManifest GetManifestFromWorkload(WorkloadId workloadId); diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs index 02356a281226..d05a243cab5b 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs @@ -6,6 +6,7 @@ using Microsoft.DotNet.Cli; using Microsoft.DotNet.Workloads.Workload; using Microsoft.NET.Sdk.Localization; +using static Microsoft.NET.Sdk.WorkloadManifestReader.IWorkloadManifestProvider; namespace Microsoft.NET.Sdk.WorkloadManifestReader { @@ -243,28 +244,29 @@ void ThrowExceptionIfManifestsNotAvailable() } } - public WorkloadVersion GetWorkloadVersion() + public WorkloadVersionInfo GetWorkloadVersion() { if (_globalJsonWorkloadSetVersion != null) { - return new WorkloadVersion() - { - Version = _globalJsonWorkloadSetVersion, - WorkloadInstallType = WorkloadVersion.Type.WorkloadSet - }; + // _exceptionToThrow is set to null here if and only if the workload set is not installed. + // If this came from --info or workload --version, the error won't be thrown, but we should still + // suggest running `dotnet workload restore` to the user. + return new WorkloadVersionInfo(_globalJsonWorkloadSetVersion, _exceptionToThrow?.Message); } ThrowExceptionIfManifestsNotAvailable(); if (_workloadSet?.Version is not null) { - return new WorkloadVersion() - { - Version = _workloadSet.Version, - WorkloadInstallType = WorkloadVersion.Type.WorkloadSet - }; + return new WorkloadVersionInfo(_workloadSet.Version); } + var installStateFilePath = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkVersionBand, _sdkOrUserLocalPath), "default.json"); + var installState = InstallStateContents.FromPath(installStateFilePath)!; + string? shouldRestoreMessage = installState.UseWorkloadSets == true ? + Strings.ShouldInstallAWorkloadSet : + null; + using (SHA256 sha256Hash = SHA256.Create()) { byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(string.Join(";", @@ -279,11 +281,7 @@ public WorkloadVersion GetWorkloadVersion() sb.Append(bytes[b].ToString("x2")); } - return new WorkloadVersion() - { - Version = $"{_sdkVersionBand.ToStringWithoutPrerelease()}-manifests.{sb}", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + return new WorkloadVersionInfo($"{_sdkVersionBand.ToStringWithoutPrerelease()}-manifests.{sb}", null, UpdateModeMessage: shouldRestoreMessage); } } diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx index cf418daa75cb..1f4d01f08f1b 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx @@ -201,6 +201,9 @@ Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. {Locked="dotnet workload restore"} + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workload version {0} was not found. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/TempDirectoryWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/TempDirectoryWorkloadManifestProvider.cs index 0c8556542108..8aee9c940b02 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/TempDirectoryWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/TempDirectoryWorkloadManifestProvider.cs @@ -53,11 +53,7 @@ public IEnumerable GetManifestDirectories() } public string GetSdkFeatureBand() => _sdkVersionBand; - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion - { - Version = _sdkVersionBand + ".2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo(_sdkVersionBand.ToString() + ".2"); public Dictionary GetAvailableWorkloadSets() => new(); } } diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadResolver.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadResolver.cs index aeac81c10abd..8c8828a04070 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadResolver.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadResolver.cs @@ -115,7 +115,7 @@ public void RefreshWorkloadManifests() InitializeManifests(); } - public WorkloadVersion GetWorkloadVersion() => _manifestProvider.GetWorkloadVersion(); + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => _manifestProvider.GetWorkloadVersion(); private void LoadManifestsFromProvider(IWorkloadManifestProvider manifestProvider) { @@ -778,11 +778,7 @@ public void RefreshWorkloadManifests() { } public Dictionary GetAvailableWorkloadSets() => new(); public IEnumerable GetManifests() => Enumerable.Empty(); public string GetSdkFeatureBand() => _sdkFeatureBand; - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion - { - Version = _sdkFeatureBand + ".2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo(_sdkFeatureBand + ".2"); } } diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf index 1891cc4ff30b..7b21fe4a0b17 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf @@ -117,6 +117,11 @@ Přesměrování úlohy {0} má jiné klíče než redirect-to. + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Neočekávaný token {0} u posunu {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf index aa35c3838c22..79f35f0b27e1 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf @@ -117,6 +117,11 @@ Die Umleitungsworkload „{0}“ hat andere Schlüssel als „redirect-to“. + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Unerwartetes Token "{0}" bei Offset {1}. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf index 485e6e0a0cf8..58541aa6f25d 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf @@ -117,6 +117,11 @@ La carga de trabajo de redireccionamiento '{0}' tiene claves distintas que las de 'redirect-to' + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Token "{0}" inesperado en el desplazamiento {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf index 0ae7f6800bd3..ec48007fbc8d 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf @@ -117,6 +117,11 @@ La charge de travail de redirection « {0} » a des clés autres que « redirection vers ». + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Jeton '{0}' inattendu à l'offset {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf index 7c666799e412..4c9164f7fd5c 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf @@ -117,6 +117,11 @@ Il carico di lavoro '{0}' di reindirizzamento ha chiavi diverse da ' Redirect-to ' + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Token '{0}' imprevisto alla posizione di offset {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf index cc172807eaa9..6cf7ba208f34 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf @@ -117,6 +117,11 @@ リダイレクト ワークロード '{0}' に 'redirect-to' 以外のキーがあります + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} オフセット {1} に予期しないトークン '{0}' があります diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf index 385531b1c48b..86a2f76a3b1b 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf @@ -117,6 +117,11 @@ 리디렉션 워크로드 '{0}'에 'redirect-to' 이외의 다른 키가 있습니다 + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} 오프셋 {1}에 예기치 않은 토큰 '{0}'이(가) 있습니다. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf index 2f5dc549ddf0..c419d8fe7264 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf @@ -117,6 +117,11 @@ Obciążenie przekierowania „{0}” ma klucze inne niż „redirect-to” + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Nieoczekiwany token „{0}” pod przesunięciem {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf index f5f0331a25ec..ceea3b2edb17 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf @@ -117,6 +117,11 @@ A carga de trabalho de redirecionamento '{0}' tem chaves diferentes além de 'redirecionar-para' + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Token inesperado '{0}' no deslocamento {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf index bf88a6af7bbe..bd09fe8168f6 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf @@ -117,6 +117,11 @@ Перенаправление рабочей нагрузки "{0}" содержит ключи, отличные от "redirect-to" + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} Непредвиденный токен "{0}" в смещении {1} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf index 786f001a3435..c343ea496921 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf @@ -117,6 +117,11 @@ '{0}' yeniden yönlendirme iş yükünde 'redirect-to' dışında anahtarlar var + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} {1} uzaklığında beklenmeyen '{0}' belirteci diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf index 911a2731d284..a31e3270aa9f 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf @@ -117,6 +117,11 @@ 重定向工作负荷“{0}”具有“重定向到”以外的键 + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} 偏移为 {1} 时意外出现的标记“{0}” diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf index 423e6216ecca..285d72e53369 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf @@ -117,6 +117,11 @@ 重新導向工作負載 '{0}' 具有 'redirect-to' 以外的其他金鑰 + + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + Workloads are configured to install and update using workload versions, but none were found. Run "dotnet workload restore" to install a workload version. + + Unexpected token '{0}' at offset {1} 位移 {1} 有未預期的權杖 '{0}' diff --git a/src/SourceBuild/content/Directory.Build.props b/src/SourceBuild/content/Directory.Build.props index fdab92fe21be..4e1ff870a250 100644 --- a/src/SourceBuild/content/Directory.Build.props +++ b/src/SourceBuild/content/Directory.Build.props @@ -172,7 +172,6 @@ $([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'prebuilt-report')) $([MSBuild]::NormalizeDirectory('$(PackageReportDir)', 'prebuilt-packages')) $([MSBuild]::NormalizeDirectory('$(PackageReportDir)', '$(MSBuildProjectName)')) - $([MSBuild]::NormalizeDirectory('$(SrcDir)', 'source-build-reference-packages', 'src')) $([MSBuild]::NormalizeDirectory('$(PrereqsPackagesDir)', 'reference')) Private.SourceBuilt.Artifacts diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/LicenseScanTests/LicenseExclusions.txt b/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/LicenseScanTests/LicenseExclusions.txt index b8d722159e49..8ae494ab03d4 100644 --- a/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/LicenseScanTests/LicenseExclusions.txt +++ b/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/LicenseScanTests/LicenseExclusions.txt @@ -173,6 +173,7 @@ src/runtime/src/libraries/System.Text.Json/roadmap/images/higher-level-component # False positive src/sdk/THIRD-PARTY-NOTICES.TXT|unknown-license-reference +src/sdk/src/SourceBuild/patches/fsharp/0001-Fix17713-Reverting-PR-17649-Make-the-interaction-bet.patch|unknown-license-reference # Configuration, doesn't apply to source directly src/sdk/src/VirtualMonoRepo/THIRD-PARTY-NOTICES.template.txt diff --git a/src/SourceBuild/patches/fsharp/0001-Fix17713-Reverting-PR-17649-Make-the-interaction-bet.patch b/src/SourceBuild/patches/fsharp/0001-Fix17713-Reverting-PR-17649-Make-the-interaction-bet.patch new file mode 100644 index 000000000000..2ac007ef3473 --- /dev/null +++ b/src/SourceBuild/patches/fsharp/0001-Fix17713-Reverting-PR-17649-Make-the-interaction-bet.patch @@ -0,0 +1,575 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: "Kevin Ransom (msft)" +Date: Mon, 16 Sep 2024 13:21:58 -0700 +Subject: [PATCH] Fix17713 - Reverting PR - 17649 - Make the interaction + between #line and #nowarn directives consistent (#17724) + +Backport: https://github.com/dotnet/fsharp/pull/17724 + +--- + buildtools/fsyacc/fsyaccdriver.fs | 2 - + .../.FSharp.Compiler.Service/9.0.100.md | 1 - + src/Compiler/AbstractIL/ilpars.fsy | 1 - + src/Compiler/Driver/CompilerDiagnostics.fs | 25 +++++---- + src/Compiler/Driver/CompilerDiagnostics.fsi | 3 +- + src/Compiler/Driver/ParseAndCheckInputs.fs | 4 +- + src/Compiler/Driver/fsc.fs | 2 +- + src/Compiler/FSComp.txt | 3 +- + src/Compiler/Facilities/LanguageFeatures.fs | 3 -- + src/Compiler/Facilities/LanguageFeatures.fsi | 1 - + src/Compiler/Service/IncrementalBuild.fs | 2 +- + src/Compiler/Service/TransparentCompiler.fs | 7 +-- + src/Compiler/pars.fsy | 1 - + src/Compiler/pppars.fsy | 1 - + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.de.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.es.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.fr.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.it.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.ja.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.ko.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.pl.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.ru.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.tr.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 -- + src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 -- + .../CompilerDirectives/Nowarn.fs | 51 ------------------- + .../FSharp.Compiler.ComponentTests.fsproj | 1 - + 29 files changed, 22 insertions(+), 151 deletions(-) + delete mode 100644 tests/FSharp.Compiler.ComponentTests/CompilerDirectives/Nowarn.fs + +diff --git a/buildtools/fsyacc/fsyaccdriver.fs b/buildtools/fsyacc/fsyaccdriver.fs +index f46e607f9..c9ca12e47 100644 +--- a/buildtools/fsyacc/fsyaccdriver.fs ++++ b/buildtools/fsyacc/fsyaccdriver.fs +@@ -199,8 +199,6 @@ let writeSpecToFile (generatorState: GeneratorState) (spec: ParserSpec) (compile + writer.WriteLineInterface "module %s" s; + + writer.WriteLine "#nowarn \"64\";; // turn off warnings that type variables used in production annotations are instantiated to concrete type"; +- writer.WriteLine "#nowarn \"1182\" // the generated code often has unused variable 'parseState'" +- writer.WriteLine "#nowarn \"3261\" // the generated code would need to properly annotate nulls, e.g. changing System.Object to `obj|null`"; + + for s in generatorState.opens do + writer.WriteLine "open %s" s; +diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md +index 22eee3e2e..91b91f3c3 100644 +--- a/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md ++++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md +@@ -1,6 +1,5 @@ + ### Fixed + +-* Fix a bug in the interaction between ``#line` and `#nowarn` directives ([PR #17649](https://github.com/dotnet/fsharp/pull/17649)) + * Fix wrong TailCall warning ([Issue #17604](https://github.com/dotnet/fsharp/issues/17604), [PR #17637](https://github.com/dotnet/fsharp/pull/17637)) + * Compiler hangs when compiling inline recursive invocation ([Issue #17376](https://github.com/dotnet/fsharp/issues/17376), [PR #17394](https://github.com/dotnet/fsharp/pull/17394)) + * Fix reporting IsFromComputationExpression only for CE builder type constructors and let bindings. ([PR #17375](https://github.com/dotnet/fsharp/pull/17375)) +diff --git a/src/Compiler/AbstractIL/ilpars.fsy b/src/Compiler/AbstractIL/ilpars.fsy +index b8380364f..ca06f6570 100644 +--- a/src/Compiler/AbstractIL/ilpars.fsy ++++ b/src/Compiler/AbstractIL/ilpars.fsy +@@ -2,7 +2,6 @@ + + %{ + +-#nowarn "64" // turn off warnings that type variables used in production annotations are instantiated to concrete type + #nowarn "1182" // the generated code often has unused variable "parseState" + #nowarn "3261" // the generated code would need to properly annotate nulls, e.g. changing System.Object to `obj|null` + +diff --git a/src/Compiler/Driver/CompilerDiagnostics.fs b/src/Compiler/Driver/CompilerDiagnostics.fs +index c73b8e5d1..1c50ca267 100644 +--- a/src/Compiler/Driver/CompilerDiagnostics.fs ++++ b/src/Compiler/Driver/CompilerDiagnostics.fs +@@ -24,7 +24,6 @@ open FSharp.Compiler.ConstraintSolver + open FSharp.Compiler.DiagnosticMessage + open FSharp.Compiler.Diagnostics + open FSharp.Compiler.DiagnosticsLogger +-open FSharp.Compiler.Features + open FSharp.Compiler.Infos + open FSharp.Compiler.IO + open FSharp.Compiler.Lexhelp +@@ -2300,13 +2299,17 @@ type PhasedDiagnostic with + // Scoped #nowarn pragmas + + /// Build an DiagnosticsLogger that delegates to another DiagnosticsLogger but filters warnings turned off by the given pragma declarations ++// ++// NOTE: we allow a flag to turn of strict file checking. This is because file names sometimes don't match due to use of ++// #line directives, e.g. for pars.fs/pars.fsy. In this case we just test by line number - in most cases this is sufficient ++// because we install a filtering error handler on a file-by-file basis for parsing and type-checking. ++// However this is indicative of a more systematic problem where source-line ++// sensitive operations (lexfilter and warning filtering) do not always ++// interact well with #line directives. + type DiagnosticsLoggerFilteringByScopedPragmas +- (langVersion: LanguageVersion, scopedPragmas, diagnosticOptions: FSharpDiagnosticOptions, diagnosticsLogger: DiagnosticsLogger) = ++ (checkFile, scopedPragmas, diagnosticOptions: FSharpDiagnosticOptions, diagnosticsLogger: DiagnosticsLogger) = + inherit DiagnosticsLogger("DiagnosticsLoggerFilteringByScopedPragmas") + +- let needCompatibilityWithEarlierInconsistentInteraction = +- not (langVersion.SupportsFeature LanguageFeature.ConsistentNowarnLineDirectiveInteraction) +- + let mutable realErrorPresent = false + + override _.DiagnosticSink(diagnostic: PhasedDiagnostic, severity) = +@@ -2320,10 +2323,12 @@ type DiagnosticsLoggerFilteringByScopedPragmas + match diagnostic.Range with + | Some m -> + scopedPragmas +- |> List.exists (fun (ScopedPragma.WarningOff(pragmaRange, warningNumFromPragma)) -> ++ |> List.exists (fun pragma -> ++ let (ScopedPragma.WarningOff(pragmaRange, warningNumFromPragma)) = pragma ++ + warningNum = warningNumFromPragma +- && (needCompatibilityWithEarlierInconsistentInteraction +- || m.FileIndex = pragmaRange.FileIndex && posGeq m.Start pragmaRange.Start)) ++ && (not checkFile || m.FileIndex = pragmaRange.FileIndex) ++ && posGeq m.Start pragmaRange.Start) + |> not + | None -> true + +@@ -2339,5 +2344,5 @@ type DiagnosticsLoggerFilteringByScopedPragmas + + override _.CheckForRealErrorsIgnoringWarnings = realErrorPresent + +-let GetDiagnosticsLoggerFilteringByScopedPragmas (langVersion, scopedPragmas, diagnosticOptions, diagnosticsLogger) = +- DiagnosticsLoggerFilteringByScopedPragmas(langVersion, scopedPragmas, diagnosticOptions, diagnosticsLogger) :> DiagnosticsLogger ++let GetDiagnosticsLoggerFilteringByScopedPragmas (checkFile, scopedPragmas, diagnosticOptions, diagnosticsLogger) = ++ DiagnosticsLoggerFilteringByScopedPragmas(checkFile, scopedPragmas, diagnosticOptions, diagnosticsLogger) :> DiagnosticsLogger +diff --git a/src/Compiler/Driver/CompilerDiagnostics.fsi b/src/Compiler/Driver/CompilerDiagnostics.fsi +index 7c5acef17..6139da434 100644 +--- a/src/Compiler/Driver/CompilerDiagnostics.fsi ++++ b/src/Compiler/Driver/CompilerDiagnostics.fsi +@@ -7,7 +7,6 @@ open System.Text + open FSharp.Compiler.CompilerConfig + open FSharp.Compiler.Diagnostics + open FSharp.Compiler.DiagnosticsLogger +-open FSharp.Compiler.Features + open FSharp.Compiler.Syntax + open FSharp.Compiler.Text + +@@ -85,7 +84,7 @@ type PhasedDiagnostic with + + /// Get a diagnostics logger that filters the reporting of warnings based on scoped pragma information + val GetDiagnosticsLoggerFilteringByScopedPragmas: +- langVersion: LanguageVersion * ++ checkFile: bool * + scopedPragmas: ScopedPragma list * + diagnosticOptions: FSharpDiagnosticOptions * + diagnosticsLogger: DiagnosticsLogger -> +diff --git a/src/Compiler/Driver/ParseAndCheckInputs.fs b/src/Compiler/Driver/ParseAndCheckInputs.fs +index d5d18d796..a6804bfe7 100644 +--- a/src/Compiler/Driver/ParseAndCheckInputs.fs ++++ b/src/Compiler/Driver/ParseAndCheckInputs.fs +@@ -511,7 +511,7 @@ let ParseInput + finally + // OK, now commit the errors, since the ScopedPragmas will (hopefully) have been scraped + let filteringDiagnosticsLogger = +- GetDiagnosticsLoggerFilteringByScopedPragmas(lexbuf.LanguageVersion, scopedPragmas, diagnosticOptions, diagnosticsLogger) ++ GetDiagnosticsLoggerFilteringByScopedPragmas(false, scopedPragmas, diagnosticOptions, diagnosticsLogger) + + delayLogger.CommitDelayedDiagnostics filteringDiagnosticsLogger + +@@ -1429,7 +1429,7 @@ let CheckOneInput + + // Within a file, equip loggers to locally filter w.r.t. scope pragmas in each input + let DiagnosticsLoggerForInput (tcConfig: TcConfig, input: ParsedInput, oldLogger) = +- GetDiagnosticsLoggerFilteringByScopedPragmas(tcConfig.langVersion, input.ScopedPragmas, tcConfig.diagnosticsOptions, oldLogger) ++ GetDiagnosticsLoggerFilteringByScopedPragmas(false, input.ScopedPragmas, tcConfig.diagnosticsOptions, oldLogger) + + /// Typecheck a single file (or interactive entry into F# Interactive) + let CheckOneInputEntry (ctok, checkForErrors, tcConfig: TcConfig, tcImports, tcGlobals, prefixPathOpt) tcState input = +diff --git a/src/Compiler/Driver/fsc.fs b/src/Compiler/Driver/fsc.fs +index 9dccdec82..ac4ee1795 100644 +--- a/src/Compiler/Driver/fsc.fs ++++ b/src/Compiler/Driver/fsc.fs +@@ -745,7 +745,7 @@ let main2 + yield! pragmas + ] + +- GetDiagnosticsLoggerFilteringByScopedPragmas(tcConfig.langVersion, scopedPragmas, tcConfig.diagnosticsOptions, oldLogger) ++ GetDiagnosticsLoggerFilteringByScopedPragmas(true, scopedPragmas, tcConfig.diagnosticsOptions, oldLogger) + + SetThreadDiagnosticsLoggerNoUnwind diagnosticsLogger + +diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt +index 2e391fa55..b5a50afc7 100644 +--- a/src/Compiler/FSComp.txt ++++ b/src/Compiler/FSComp.txt +@@ -1783,5 +1783,4 @@ featureEmptyBodiedComputationExpressions,"Support for computation expressions wi + featureAllowAccessModifiersToAutoPropertiesGettersAndSetters,"Allow access modifiers to auto properties getters and setters" + 3871,tcAccessModifiersNotAllowedInSRTPConstraint,"Access modifiers cannot be applied to an SRTP constraint." + featureAllowObjectExpressionWithoutOverrides,"Allow object expressions without overrides" +-3872,tcPartialActivePattern,"Multi-case partial active patterns are not supported. Consider using a single-case partial active pattern or a full active pattern." +-featureConsistentNowarnLineDirectiveInteraction,"The interaction between #nowarn and #line is now consistent." ++3872,tcPartialActivePattern,"Multi-case partial active patterns are not supported. Consider using a single-case partial active pattern or a full active pattern." +\ No newline at end of file +diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs +index 5f16aead3..5d71f3ed2 100644 +--- a/src/Compiler/Facilities/LanguageFeatures.fs ++++ b/src/Compiler/Facilities/LanguageFeatures.fs +@@ -94,7 +94,6 @@ type LanguageFeature = + | ParsedHashDirectiveArgumentNonQuotes + | EmptyBodiedComputationExpressions + | AllowObjectExpressionWithoutOverrides +- | ConsistentNowarnLineDirectiveInteraction + + /// LanguageVersion management + type LanguageVersion(versionText) = +@@ -213,7 +212,6 @@ type LanguageVersion(versionText) = + LanguageFeature.LowerSimpleMappingsInComprehensionsToFastLoops, languageVersion90 + LanguageFeature.ParsedHashDirectiveArgumentNonQuotes, languageVersion90 + LanguageFeature.EmptyBodiedComputationExpressions, languageVersion90 +- LanguageFeature.ConsistentNowarnLineDirectiveInteraction, languageVersion90 + + // F# preview + LanguageFeature.EnforceAttributeTargets, previewVersion // waiting for fix of https://github.com/dotnet/fsharp/issues/17731 +@@ -377,7 +375,6 @@ type LanguageVersion(versionText) = + | LanguageFeature.ParsedHashDirectiveArgumentNonQuotes -> FSComp.SR.featureParsedHashDirectiveArgumentNonString () + | LanguageFeature.EmptyBodiedComputationExpressions -> FSComp.SR.featureEmptyBodiedComputationExpressions () + | LanguageFeature.AllowObjectExpressionWithoutOverrides -> FSComp.SR.featureAllowObjectExpressionWithoutOverrides () +- | LanguageFeature.ConsistentNowarnLineDirectiveInteraction -> FSComp.SR.featureConsistentNowarnLineDirectiveInteraction () + + /// Get a version string associated with the given feature. + static member GetFeatureVersionString feature = +diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi +index 4ae722c7f..7408300b9 100644 +--- a/src/Compiler/Facilities/LanguageFeatures.fsi ++++ b/src/Compiler/Facilities/LanguageFeatures.fsi +@@ -85,7 +85,6 @@ type LanguageFeature = + | ParsedHashDirectiveArgumentNonQuotes + | EmptyBodiedComputationExpressions + | AllowObjectExpressionWithoutOverrides +- | ConsistentNowarnLineDirectiveInteraction + + /// LanguageVersion management + type LanguageVersion = +diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs +index 7951f3c93..b7560b222 100644 +--- a/src/Compiler/Service/IncrementalBuild.fs ++++ b/src/Compiler/Service/IncrementalBuild.fs +@@ -259,7 +259,7 @@ type BoundModel private ( + + IncrementalBuilderEventTesting.MRU.Add(IncrementalBuilderEventTesting.IBETypechecked fileName) + let capturingDiagnosticsLogger = CapturingDiagnosticsLogger("TypeCheck") +- let diagnosticsLogger = GetDiagnosticsLoggerFilteringByScopedPragmas(tcConfig.langVersion, input.ScopedPragmas, tcConfig.diagnosticsOptions, capturingDiagnosticsLogger) ++ let diagnosticsLogger = GetDiagnosticsLoggerFilteringByScopedPragmas(false, input.ScopedPragmas, tcConfig.diagnosticsOptions, capturingDiagnosticsLogger) + use _ = new CompilationGlobalsScope(diagnosticsLogger, BuildPhase.TypeCheck) + + beforeFileChecked.Trigger fileName +diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs +index 735a6b241..e3acd1d4c 100644 +--- a/src/Compiler/Service/TransparentCompiler.fs ++++ b/src/Compiler/Service/TransparentCompiler.fs +@@ -1303,12 +1303,7 @@ type internal TransparentCompiler + let diagnosticsLogger = errHandler.DiagnosticsLogger + + let diagnosticsLogger = +- GetDiagnosticsLoggerFilteringByScopedPragmas( +- tcConfig.langVersion, +- input.ScopedPragmas, +- tcConfig.diagnosticsOptions, +- diagnosticsLogger +- ) ++ GetDiagnosticsLoggerFilteringByScopedPragmas(false, input.ScopedPragmas, tcConfig.diagnosticsOptions, diagnosticsLogger) + + use _ = new CompilationGlobalsScope(diagnosticsLogger, BuildPhase.TypeCheck) + +diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy +index 54d47b7c4..2794edf56 100644 +--- a/src/Compiler/pars.fsy ++++ b/src/Compiler/pars.fsy +@@ -2,7 +2,6 @@ + + %{ + +-#nowarn "64" // turn off warnings that type variables used in production annotations are instantiated to concrete type + #nowarn "1182" // generated code has lots of unused "parseState" + #nowarn "3261" // the generated code would need to properly annotate nulls, e.g. changing System.Object to `obj|null` + +diff --git a/src/Compiler/pppars.fsy b/src/Compiler/pppars.fsy +index 41cb41ff3..cd27722a2 100644 +--- a/src/Compiler/pppars.fsy ++++ b/src/Compiler/pppars.fsy +@@ -3,7 +3,6 @@ + %{ + open FSharp.Compiler.DiagnosticsLogger + +-#nowarn "64" // turn off warnings that type variables used in production annotations are instantiated to concrete type + #nowarn "3261" // the generated code would need to properly annotate nulls, e.g. changing System.Object to `obj|null` + + let dummy = IfdefId("DUMMY") +diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf +index 39b41f3b3..2b1a9483d 100644 +--- a/src/Compiler/xlf/FSComp.txt.cs.xlf ++++ b/src/Compiler/xlf/FSComp.txt.cs.xlf +@@ -307,11 +307,6 @@ + Vyvolá upozornění, pokud je atribut TailCall použit u nerekurzivních funkcí. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Průnik omezení u flexibilních typů +diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf +index 402571c8a..a1b2532e4 100644 +--- a/src/Compiler/xlf/FSComp.txt.de.xlf ++++ b/src/Compiler/xlf/FSComp.txt.de.xlf +@@ -307,11 +307,6 @@ + Löst Warnungen aus, wenn das Attribut "TailCall" für nicht rekursive Funktionen verwendet wird. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Einschränkungsüberschneidung für flexible Typen +diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf +index d3267a742..dd9cbb7fe 100644 +--- a/src/Compiler/xlf/FSComp.txt.es.xlf ++++ b/src/Compiler/xlf/FSComp.txt.es.xlf +@@ -307,11 +307,6 @@ + Genera advertencias si el atributo 'TailCall' se usa en funciones no recursivas. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Intersección de restricciones en tipos flexibles +diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf +index f74bc4e31..2cc92e5f4 100644 +--- a/src/Compiler/xlf/FSComp.txt.fr.xlf ++++ b/src/Compiler/xlf/FSComp.txt.fr.xlf +@@ -307,11 +307,6 @@ + Émet des avertissements si l’attribut « TailCall » est utilisé sur des fonctions non récursives. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Intersection de contraintes sur les types flexibles +diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf +index 3495d0c16..5b5793c48 100644 +--- a/src/Compiler/xlf/FSComp.txt.it.xlf ++++ b/src/Compiler/xlf/FSComp.txt.it.xlf +@@ -307,11 +307,6 @@ + Genera avvisi se l'attributo 'TailCall' viene utilizzato in funzioni non ricorsive. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Intersezione di vincoli su tipi flessibili +diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf +index a87915633..d30efc747 100644 +--- a/src/Compiler/xlf/FSComp.txt.ja.xlf ++++ b/src/Compiler/xlf/FSComp.txt.ja.xlf +@@ -307,11 +307,6 @@ + 'TailCall' 属性が再帰関数以外で使用されている場合、警告が発せられます。 + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + フレキシブル型の制約積集合 +diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf +index 70a64e75a..c9359c568 100644 +--- a/src/Compiler/xlf/FSComp.txt.ko.xlf ++++ b/src/Compiler/xlf/FSComp.txt.ko.xlf +@@ -307,11 +307,6 @@ + 'TailCall' 특성이 비 재귀 함수에 사용되는 경우 경고를 발생합니다. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + 유연한 형식의 제약 조건 교집합 +diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf +index 31b8d2a62..933379b9f 100644 +--- a/src/Compiler/xlf/FSComp.txt.pl.xlf ++++ b/src/Compiler/xlf/FSComp.txt.pl.xlf +@@ -307,11 +307,6 @@ + Zgłasza ostrzeżenia, jeśli atrybut „TailCall” jest używany w funkcjach niekursywnych. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Przecięcie ograniczenia dla typów elastycznych +diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +index c49d815ca..e6480e130 100644 +--- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf ++++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +@@ -307,11 +307,6 @@ + Gera avisos se o atributo "TailCall" for usado em funções não recursivas. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Interseção de restrição em tipos flexíveis +diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf +index 495089d8c..61c038859 100644 +--- a/src/Compiler/xlf/FSComp.txt.ru.xlf ++++ b/src/Compiler/xlf/FSComp.txt.ru.xlf +@@ -307,11 +307,6 @@ + Выдает предупреждения, если атрибут TailCall используется в нерекурсивных функциях. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Пересечение ограничений на гибких типах +diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf +index 93dfdedb2..09c113ee3 100644 +--- a/src/Compiler/xlf/FSComp.txt.tr.xlf ++++ b/src/Compiler/xlf/FSComp.txt.tr.xlf +@@ -307,11 +307,6 @@ + 'TailCall' özniteliği özyinelemeli olmayan işlevlerde kullanılıyorsa uyarılar oluşturur. + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + Esnek türlerde kısıtlama kesişimi +diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +index 54f6b0880..d9573b5be 100644 +--- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf ++++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +@@ -307,11 +307,6 @@ + 如果在非递归函数上使用“TailCall”属性,则引发警告。 + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + 灵活类型的约束交集 +diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +index df3a706ae..1a6c3de6c 100644 +--- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf ++++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +@@ -307,11 +307,6 @@ + 如果 'TailCall' 屬性用於非遞迴函數,則引發警告。 + + +- +- The interaction between #nowarn and #line is now consistent. +- The interaction between #nowarn and #line is now consistent. +- +- + + Constraint intersection on flexible types + 彈性類型上的條件約束交集 +diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerDirectives/Nowarn.fs b/tests/FSharp.Compiler.ComponentTests/CompilerDirectives/Nowarn.fs +deleted file mode 100644 +index 78067aa8c..000000000 +--- a/tests/FSharp.Compiler.ComponentTests/CompilerDirectives/Nowarn.fs ++++ /dev/null +@@ -1,51 +0,0 @@ +-// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +-namespace CompilerDirectives +- +-open Xunit +-open FSharp.Test.Compiler +- +-module Nowarn = +- +- let warn20Text = "The result of this expression has type 'string' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'." +- +- let checkFileBugSource = """ +-module A +-#nowarn "20" +-#line 1 "xyz.fs" +-"" +- """ +- +- let checkFileBugSource2 = """ +-module A +-#line 1 "xyz.fs" +-#nowarn "20" +-"" +- """ +- +- +- [] +- let ``checkFile bug simulation for compatibility`` () = +- +- FSharp checkFileBugSource +- |> withLangVersion80 +- |> compile +- |> shouldSucceed +- +- [] +- let ``checkFile bug fixed leads to new warning`` () = +- +- FSharp checkFileBugSource +- |> withLangVersion90 +- |> compile +- |> shouldFail +- |> withDiagnostics [ +- (Warning 20, Line 1, Col 1, Line 1, Col 3, warn20Text) +- ] +- +- [] +- let ``checkFile bug fixed, no warning if nowarn is correctly used`` () = +- +- FSharp checkFileBugSource2 +- |> withLangVersion90 +- |> compile +- |> shouldSucceed +diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +index f7e63b796..5f352a315 100644 +--- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj ++++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +@@ -33,7 +33,6 @@ + + + +- + + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.EolTargetFrameworks.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.EolTargetFrameworks.targets index d9bd74aa368f..8608d9f77a33 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.EolTargetFrameworks.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.EolTargetFrameworks.targets @@ -21,7 +21,7 @@ Copyright (c) .NET Foundation. All rights reserved. receive servicing updates and security fixes. --> - <_EolNetCoreTargetFrameworkVersions Include="1.0;1.1;2.0;2.1;2.2;3.0;3.1;5.0" /> + <_EolNetCoreTargetFrameworkVersions Include="1.0;1.1;2.0;2.1;2.2;3.0;3.1;5.0;7.0" /> <_MinimumNonEolSupportedNetCoreTargetFramework>net6.0 diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToTargetEolFrameworks.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToTargetEolFrameworks.cs index 9f196512336e..6e020c0df98f 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToTargetEolFrameworks.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToTargetEolFrameworks.cs @@ -15,6 +15,7 @@ public GivenThatWeWantToTargetEolFrameworks(ITestOutputHelper log) : base(log) [InlineData("netcoreapp3.0")] [InlineData("netcoreapp3.1")] [InlineData("net5.0")] + [InlineData("net7.0")] public void It_warns_that_framework_is_out_of_support(string targetFrameworks) { var testProject = new TestProject() diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs index a322cc2dd46c..dd583d119f6e 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs @@ -44,7 +44,7 @@ public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_enabled(string var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); publishCommand - .Execute($"/p:UseCurrentRuntimeIdentifier=true", "/p:SelfContained=true") + .Execute($"/p:UseCurrentRuntimeIdentifier=true", "/p:SelfContained=true", "/p:CheckEolTargetFramework=false") .Should().Pass() .And.NotHaveStdOutContaining("IL2026") .And.NotHaveStdErrContaining("NETSDK1179") @@ -88,7 +88,7 @@ public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_false(string t var publishCommand = new PublishCommand(testAsset); publishCommand - .Execute($"/p:RuntimeIdentifier={rid}", "/p:SelfContained=true") + .Execute($"/p:RuntimeIdentifier={rid}", "/p:SelfContained=true", "/p:CheckEolTargetFramework=false") .Should().Pass() .And.NotHaveStdOutContaining("IL2026") .And.NotHaveStdErrContaining("NETSDK1179") diff --git a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/FakeManifestProvider.cs b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/FakeManifestProvider.cs index d2fbfbc10063..09dad2d93f12 100644 --- a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/FakeManifestProvider.cs +++ b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/FakeManifestProvider.cs @@ -40,11 +40,7 @@ public IEnumerable GetManifests() public string GetSdkFeatureBand() => "8.0.100"; public Dictionary GetAvailableWorkloadSets() => throw new NotImplementedException(); - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion - { - Version = "8.0.100.2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo("8.0.100.2"); } internal class InMemoryFakeManifestProvider : IWorkloadManifestProvider, IEnumerable<(string id, string content)> @@ -71,10 +67,6 @@ public IEnumerable GetManifests() IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); public string GetSdkFeatureBand() => "8.0.100"; public Dictionary GetAvailableWorkloadSets() => throw new NotImplementedException(); - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion - { - Version = "8.0.100.2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo("8.0.100.2"); } } diff --git a/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs b/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs new file mode 100644 index 000000000000..69d06b8c0479 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs @@ -0,0 +1,532 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.Net; +using System.Net.Http.Headers; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using Microsoft.WebTools.AspireServer.Helpers; +using Microsoft.WebTools.AspireServer.Models; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +public class AspireServerServiceTests +{ + private const string Project1Path = @"c:\test\Projects\project1.csproj"; + private const int ProcessId = 34213; + private const string DcpId = "myid"; + private const string SpecificProfileName = "SpecificProfile"; + private const string VersionedSessionUrl = $"{RunSessionRequest.Url}?{RunSessionRequest.VersionQuery}={RunSessionRequest.OurProtocolVersion}"; + + private static readonly TestRunSessionRequest Project1SessionRequest = new TestRunSessionRequest(Project1Path, debugging: false, launchProfile: null, disableLaunchProfile: false) + { + args = new List { "--project1Arg" }, + env = new List { new EnvVar { Name = "var1", Value = "value1" } } + }; + + [Fact] + public async Task SessionStarted_Test() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + // Start listening + TaskCompletionSource connected = new(); + + TaskCompletionSource notificationTask = new(); + _ = listenForSessionUpdatesAsync(server, connected, (sn) => + { + notificationTask.SetResult((SessionChangeNotification)sn); + }); + + await connected.Task; + + await server.SessionStartedAsync(DcpId,"1", ProcessId, CancellationToken.None); + + var result = await notificationTask.Task; + + Assert.Equal(ProcessId, result.PID); + Assert.Equal("1", result.SessionId); + + await server.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task SessionEndedAsync_Test() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + // Start listening + TaskCompletionSource connected = new(); + TaskCompletionSource sessionEndNotificationTask = new(); + _ = listenForSessionUpdatesAsync(server, connected, (sn) => + { + if (sn.NotificationType == NotificationType.SessionTerminated) + { + sessionEndNotificationTask.SetResult((SessionChangeNotification)sn); + } + }); + + await connected.Task; + + await server.SessionEndedAsync(DcpId, "1", ProcessId, 130, CancellationToken.None); + + var result = await sessionEndNotificationTask.Task; + Assert.Equal(ProcessId, result.PID); + Assert.Equal("1", result.SessionId); + Assert.Equal(130, result.ExitCode); + + await server.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_Success() + { + var mocks = new Mocks(); + + mocks.GetOrCreate() + .ImplementStartProjectAsync(DcpId, "2"); + + var server = await GetAspireServer(mocks); + var tokens = await server.GetServerVariablesAsync(); + + using HttpClient client = GetHttpClient(tokens); + + HttpResponseMessage response; + response = await client.PutAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal($"{client.BaseAddress}run_session/2", response.Headers.Location.AbsoluteUri); + + await server.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_Success_ThenStopProcessRequest() + { + var mocks = new Mocks(); + + mocks.GetOrCreate() + .ImplementStartProjectAsync(DcpId, "2") + .ImplementStopSessionAsync(DcpId, "2", exists: true) + .ImplementStopSessionAsync(DcpId, "3", exists: false); + + var server = await GetAspireServer(mocks); + var tokens = await server.GetServerVariablesAsync(); + + using HttpClient client = GetHttpClient(tokens); + + var response = await client.PutAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + // Now send a stop session + response = await client.DeleteAsync(RunSessionRequest.Url + "/2"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Validate NoContent response if session not found + response = await client.DeleteAsync(RunSessionRequest.Url + "/3"); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + await server.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_FailedToLaunchProject() + { + var mocks = new Mocks(); + + mocks.GetOrCreate() + .ImplementStartProjectAsync(DcpId, "2", new Exception("Launch project failed")); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + + var response = await client.PutAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.Equal("{\"error\":{\"message\":\"Launch project failed\"}}", await response.Content.ReadAsStringAsync()); + + await server.DisposeAsync(); + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_FailNoBearerToken() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); + + var response = await client.PutAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + await server.DisposeAsync(); + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_FailWrongUrl() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + + var response = await client.PutAsJsonAsync("/run_badurl", Project1SessionRequest); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + await server.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task LaunchProject_NotAPUTRequest() + { + var mocks = new Mocks(); + + var aspireServer = await GetAspireServer(mocks); + + var tokens = await aspireServer.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + + var response = await client.PostAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); + + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + + await aspireServer.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task StopSession_FailNoBearerToken() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); + + var response = await client.DeleteAsync(RunSessionRequest.Url + "/2"); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + await server.DisposeAsync(); + mocks.Verify(); + } + + [Fact] + public async Task Info_Success() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + + var response = await client.GetAsync(InfoResponse.Url); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + await server.DisposeAsync(); + mocks.Verify(); + } + + [Fact] + public async Task Info_FailNoBearerToken() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks); + + var tokens = await server.GetServerVariablesAsync(); + using HttpClient client = GetHttpClient(tokens); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); + + var response = await client.GetAsync(InfoResponse.Url); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + await server.DisposeAsync(); + mocks.Verify(); + } + + [Fact] + public async Task SendLogMessageAsync_Test() + { + var mocks = new Mocks(); + + var aspireServer = await GetAspireServer(mocks); + + + // Start listening + TaskCompletionSource connected = new(); + TaskCompletionSource notificationTask = new(); + _ = listenForSessionUpdatesAsync(aspireServer, connected, (sn) => + { + notificationTask.SetResult((SessionLogsNotification)sn); + }); + + await connected.Task; + + await aspireServer.SendLogMessageAsync(DcpId, "1", isStdErr: false, "My Message", CancellationToken.None); + + var result = await notificationTask.Task; + + Assert.Equal("My Message", result.LogMessage); + Assert.False(result.IsStdErr); + await aspireServer.DisposeAsync(); + + mocks.Verify(); + } + + [Fact] + public async Task GetEnvironmentForOrchestrator_Tests() + { + var mocks = new Mocks(); + + var server = await GetAspireServer(mocks, waitForListening: false); + + // First time should create a key + var envVars = await server.GetServerConnectionEnvironmentAsync(CancellationToken.None); + + Assert.Equal(3, envVars.Count); + var token = envVars[1]; + Assert.NotNull(token.Value); + + // Should return the same + envVars = await server.GetServerConnectionEnvironmentAsync(CancellationToken.None); + Assert.Equal(token, envVars[1]); + + mocks.Verify(); + } + + private async Task listenForSessionUpdatesAsync(AspireServerService aspireServer, TaskCompletionSource connected, Action callback) + { + var tokens = await aspireServer.GetServerVariablesAsync(); + using var httpClient = GetHttpClient(tokens); + + using var ws = new ClientWebSocket(); + ws.Options.SetRequestHeader("Authorization", $"Bearer {tokens.bearerToken}"); + try + { + await ws.ConnectAsync(new Uri($"wss://{tokens.serverAddress}{RunSessionRequest.Url}{SessionNotificationBase.Url}"), httpClient, CancellationToken.None); + } + catch (Exception ex) + { + Assert.Fail("Could not connect to session update endpoint: " + ex.ToString()); + connected.SetResult(false); + return; + } + + connected.SetResult(true); + + while (ws.State == WebSocketState.Open) + { + try + { + var (message, messageType) = await GetSocketMsgAsync(ws); + + if (messageType == WebSocketMessageType.Close) + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + return; + } + else + { + var notificationBase = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); + if (notificationBase is null) + { + Console.WriteLine("Unexpected null SessionNotificationBase message"); + } + else if (notificationBase.NotificationType == NotificationType.ProcessRestarted || notificationBase.NotificationType == NotificationType.SessionTerminated) + { + var scn = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); + if (scn is null) + { + Assert.Fail("Unexpected null SessionChangeNotification message"); + } + else + { + callback.Invoke(scn); + } + } + else if (notificationBase.NotificationType == NotificationType.ServiceLogs) + { + var sessionLogs = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); + if (sessionLogs is null) + { + Assert.Fail("Unexpected null SessionLogsNotification message"); + } + else + { + callback.Invoke(sessionLogs); + } + } + } + } + catch + { + // This is expected if the connection is closed + return; + } + } + } + + private static HttpClient GetHttpClient((string serverAddress, string bearerToken, string certToken) tokens) + { + HttpClient client; + var serverCert = X509CertificateLoader.LoadCertificate(Convert.FromBase64String(tokens.certToken)); + var clientHandler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Manual, + SslProtocols = System.Security.Authentication.SslProtocols.None, + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => + { + return cert?.Thumbprint == serverCert.Thumbprint; + } + }; + + client = new HttpClient(clientHandler); + client.BaseAddress = new Uri($"https://{tokens.serverAddress}"); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.bearerToken); + client.DefaultRequestHeaders.Add(HttpContextExtensions.DCPInstanceIDHeader, DcpId); + + return client; + } + + private async Task<(string, WebSocketMessageType)> GetSocketMsgAsync(ClientWebSocket client) + { + var rcvBuffer = new ArraySegment(new byte[2048]); + WebSocketReceiveResult rcvResult = await client.ReceiveAsync(rcvBuffer, CancellationToken.None); + if (rcvResult.MessageType == WebSocketMessageType.Text) + { + byte[] msgBytes = rcvBuffer.Skip(rcvBuffer.Offset).Take(rcvResult.Count).ToArray(); + return (Encoding.UTF8.GetString(msgBytes), rcvResult.MessageType); + } + + return (null, rcvResult.MessageType); + } + + private async Task GetAspireServer(Mocks mocks, bool waitForListening = true) + { + var ase = mocks.GetOrCreate(); + + var aspireServer = new AspireServerService(ase.Object, displayName: "Test server", Console.WriteLine); + + if (waitForListening) + { + await aspireServer.WaitForListeningAsync(); + } + + return aspireServer; + } + +#pragma warning disable IDE1006 // Naming Styles + internal class TestRunSessionRequestP4 + { + public string project_path { get; set; } = string.Empty; + public bool debug { get; set; } + public List env { get; set; } = new List(); + public List args { get; set; } = new List(); + public string launch_profile { get; set; } + public bool disable_launch_profile { get; set; } + } + + internal class TestRunSessionRequest + { + public TestRunSessionRequest(string projectPath, bool debugging, string launchProfile, bool disableLaunchProfile) + { + launch_configurations = new TestLaunchConfiguration[] + { + new() { + project_path = projectPath, + type = RunSessionRequest.ProjectLaunchConfigurationType, + mode= debugging? RunSessionRequest.DebugLaunchMode : RunSessionRequest.NoDebugLaunchMode, + launch_profile = launchProfile, + disable_launch_profile = disableLaunchProfile + } + }; + } + public TestLaunchConfiguration[] launch_configurations { get; set; } + public List env { get; set; } = new List(); + public List args { get; set; } = new List(); + + public TestRunSessionRequestP4 ToTestRunSessionRequestP4() + { + var launchConfig = launch_configurations[0]; + return new TestRunSessionRequestP4() + { + project_path = launchConfig.project_path, + debug = string.Equals(launchConfig.mode, RunSessionRequest.DebugLaunchMode, StringComparison.OrdinalIgnoreCase), + args = args, + env = env, + launch_profile = launchConfig.launch_profile, + disable_launch_profile = launchConfig.disable_launch_profile + }; + } + } + + internal class TestLaunchConfiguration + { + public string type { get; set; } = string.Empty; + public string project_path { get; set; } = string.Empty; + public string launch_profile { get; set; } + public bool disable_launch_profile { get; set; } + public string mode { get; set; } = string.Empty; + } + + internal class TestStopSessionRequest + { + public string session_id { get; set; } = string.Empty; + } +#pragma warning restore IDE1006 // Naming Styles +} + +internal static class AspireServerServiceExtensions +{ + public static async Task WaitForListeningAsync(this AspireServerService aspireServer) + { + string serverAddress = (await aspireServer.GetServerVariablesAsync()).serverAddress; + + // We need to wait on the port being available + await Helpers.CanConnectToPortAsync(new Uri($"http://{serverAddress}"), 5000, CancellationToken.None); + + } + + public static async Task<(string serverAddress, string bearerToken, string certToken)> GetServerVariablesAsync(this AspireServerService aspireServer) + { + var enVars = await aspireServer.GetServerConnectionEnvironmentAsync(CancellationToken.None); + return (enVars[0].Value, enVars[1].Value, enVars[2].Value); + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Microsoft.WebTools.AspireService.Tests.csproj b/test/Microsoft.WebTools.AspireService.Tests/Microsoft.WebTools.AspireService.Tests.csproj new file mode 100644 index 000000000000..927652b1cd10 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Microsoft.WebTools.AspireService.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(SdkTargetFramework) + Microsoft.WebTools.AspireServer.UnitTests + enable + Exe + false + + + + + + + + + + diff --git a/test/Microsoft.WebTools.AspireService.Tests/Mocks/Helpers.cs b/test/Microsoft.WebTools.AspireService.Tests/Mocks/Helpers.cs new file mode 100644 index 000000000000..19f6d17f15c3 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Mocks/Helpers.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +public static class Helpers +{ + public static async Task CanConnectToPortAsync(Uri url, uint msToWait, CancellationToken cancelToken) + { + bool connected = false; + Socket? ipv4Socket = null; + Socket? ipv6Socket = null; + + // Create a "client" socket on any available port + try + { + TimeoutSpan timeout = new(msToWait); + if (Socket.OSSupportsIPv4) + { + try + { + ipv4Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + ipv4Socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + } + catch (SocketException) + { + if (ipv4Socket != null) + { + ipv4Socket.Close(); + ipv4Socket = null; + } + } + } + if (Socket.OSSupportsIPv6) + { + try + { + ipv6Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + ipv6Socket.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + } + catch (SocketException) + { + if (ipv6Socket != null) + { + ipv6Socket.Close(); + ipv6Socket = null; + } + } + } + + // No sockets means we aren't connected + if (ipv6Socket == null && ipv4Socket == null) + { + return false; + } + + // If we have an IP address we use that otherwise assume loopback + IPEndPoint ipv4ServerEndPoint; + IPEndPoint ipv6ServerEndPoint; + if (IPAddress.TryParse(url.Host, out var ipAddress)) + { + ipv4ServerEndPoint = new IPEndPoint(ipAddress.AddressFamily == AddressFamily.InterNetwork ? ipAddress : IPAddress.Loopback, url.Port); + ipv6ServerEndPoint = new IPEndPoint(ipAddress.AddressFamily == AddressFamily.InterNetworkV6 ? ipAddress : IPAddress.IPv6Loopback, url.Port); + } + else + { + ipv4ServerEndPoint = new IPEndPoint(IPAddress.Loopback, url.Port); + ipv6ServerEndPoint = new IPEndPoint(IPAddress.IPv6Loopback, url.Port); + } + + // If a process is passed in, we bail if it has exited + while (!connected && !timeout.Expired) + { + cancelToken.ThrowIfCancellationRequested(); + if (ipv4Socket != null) + { + try + { + // Now use IOControl to set the calls non blocking + ipv4Socket.Blocking = false; + // Since we are non-blocking, the Connect should throw an error indicating + // it needs time to connect + ipv4Socket.Connect(ipv4ServerEndPoint); + } + catch (SocketException) + { + // Now ping retry and block for a millisecond timeout + ArrayList connectList = new ArrayList() {ipv4Socket}; + Socket.Select(null, connectList, null, 1000 /*microSecond -- in here, 1 milli-second*/); + if (connectList.Count == 1) + { + connected = true; + break; + } + } + finally + { + // TODO: why do we set the sockets back to blocking? + ipv4Socket.Blocking = true; + } + } + + // Now try IPV6 + if (ipv6Socket != null) + { + // Couldn't connect with IPV4, so try IPV6 + try + { + ipv6Socket.Blocking = false; + ipv6Socket.Connect(ipv6ServerEndPoint); + } + catch (SocketException) + { + // Ping retry + ArrayList connectList = new ArrayList() {ipv6Socket}; + Socket.Select(null, connectList, null, 1000 /*microSecond -- in here, 1 milli-second*/); + if (connectList.Count == 1) + { + connected = true; + break; + } + } + finally + { + // TODO: why do we set the sockets back to blocking? + ipv6Socket.Blocking = true; + } + } + + // Wait a bit and try again + await Task.Delay(20, cancelToken); + } + + } + finally + { + if (ipv4Socket != null) + { + ipv4Socket.Close(); + } + + if (ipv6Socket != null) + { + ipv6Socket.Close(); + } + } + + return connected; + } +} + +internal class TimeoutSpan +{ + private readonly long _duration; + private long _startingTickCount; + + public TimeoutSpan(long durationInMilliseconds) + { + // There are 10000 ticks in a millisecond so need to adjust accordingly + _duration = durationInMilliseconds * 10000; + Reset(); + } + + public bool Expired + { + get + { + return _duration != 0 && (DateTime.UtcNow.Ticks - _startingTickCount) > _duration; + } + } + + public void Reset() + { + // DateTime.UtcNow is way more efficient than DateTime.Now since it doesn't have to deal with locale, DST, etc + _startingTickCount = DateTime.UtcNow.Ticks; + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Mocks/IAspireServerEventsMock.cs b/test/Microsoft.WebTools.AspireService.Tests/Mocks/IAspireServerEventsMock.cs new file mode 100644 index 000000000000..3c805871fdf4 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Mocks/IAspireServerEventsMock.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.WebTools.AspireServer.Contracts; +using Moq; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +internal class IAspireServerEventsMock : MockFactory +{ + public IAspireServerEventsMock(Mocks mocks, MockBehavior? mockBehavior = null) + : base(mocks, mockBehavior) + { + } + + public IAspireServerEventsMock ImplementStartProjectAsync(string dcpId, string sessionId, Exception? ex = null) + { + MockObject.Setup(x => x.StartProjectAsync(dcpId, It.IsAny(), It.IsAny())) + .Returns(() => + { + if (ex is not null) + { + throw ex; + } + + return new ValueTask(sessionId); + }) + .Verifiable(); + return this; + } + + public IAspireServerEventsMock ImplementStopSessionAsync(string dcpId, string sessionId, bool exists, Exception? ex = null) + { + MockObject.Setup(x => x.StopSessionAsync(dcpId, sessionId, It.IsAny())) + .Returns(() => + { + if (ex is not null) + { + throw ex; + } + + return new ValueTask(exists); + }) + .Verifiable(); + return this; + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Mocks/IServiceProviderMock.cs b/test/Microsoft.WebTools.AspireService.Tests/Mocks/IServiceProviderMock.cs new file mode 100644 index 000000000000..c975028c392f --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Mocks/IServiceProviderMock.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Moq; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +internal class IServiceProviderMock : MockFactory +{ + public IServiceProviderMock(Mocks mocks, MockBehavior? mockBehavior = null) + : base(mocks, mockBehavior) + { + } + + public IServiceProviderMock ImplementService(Type type, object service) + { + MockObject.Setup(x => x.GetService(type)).Returns(service); + + return this; + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Mocks/MockFactory.cs b/test/Microsoft.WebTools.AspireService.Tests/Mocks/MockFactory.cs new file mode 100644 index 000000000000..ef9f4467013c --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Mocks/MockFactory.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Moq; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +public interface IMockFactory +{ + void Verify(); + object GetObject(); +} + +public class MockFactory : IMockFactory where T : class +{ + public MockFactory(Mocks mocks, MockBehavior? mockBehavior) + { + AllMocks = mocks; + MockObject = new Mock(mockBehavior ?? MockBehavior.Strict); + } + + protected Mocks AllMocks { get; } + public Mock MockObject { get; } + + public T Object => MockObject.Object; + + public virtual void Verify() + { + MockObject.VerifyAll(); + } + + public object GetObject() + { + return Object; + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Mocks/Mocks.cs b/test/Microsoft.WebTools.AspireService.Tests/Mocks/Mocks.cs new file mode 100644 index 000000000000..0b546ad5b20e --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Mocks/Mocks.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Moq; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +public class Mocks +{ + private readonly Dictionary _mockFactories = new(); + + public void Add(IMockFactory factory) + { + _mockFactories.Add(factory.GetType(), factory); + } + + public T GetOrCreate(MockBehavior? mockBehavior = null) where T : IMockFactory + { + if (_mockFactories.TryGetValue(typeof(T), out var factory)) + { + return (T)factory; + } + + var newMock = (IMockFactory?)Activator.CreateInstance(typeof(T), this, mockBehavior); + Debug.Assert(newMock != null); + Add(newMock); + return (T)newMock; + } + + public virtual void Verify() + { + foreach (var factory in _mockFactories) + { + factory.Value.Verify(); + } + } +} diff --git a/test/Microsoft.WebTools.AspireService.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.WebTools.AspireService.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..8b7c37cd90c4 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/test/Microsoft.WebTools.AspireService.Tests/Properties/launchSettings.json b/test/Microsoft.WebTools.AspireService.Tests/Properties/launchSettings.json new file mode 100644 index 000000000000..2d63805b76a3 --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.WebTools.AspireServer.Test": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61653;http://localhost:61654" + } + } +} \ No newline at end of file diff --git a/test/Microsoft.WebTools.AspireService.Tests/RunSessionRequestTests.cs b/test/Microsoft.WebTools.AspireService.Tests/RunSessionRequestTests.cs new file mode 100644 index 000000000000..4012d02bf10c --- /dev/null +++ b/test/Microsoft.WebTools.AspireService.Tests/RunSessionRequestTests.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.WebTools.AspireServer.Models; + +namespace Microsoft.WebTools.AspireServer.UnitTests; + +public class RunSessionRequestTests +{ + [Fact] + public void RunSessionRequest_ToProjectLaunchRequest() + { + var runSessionReq = new RunSessionRequest() + { + Arguments = new string[] { "--someArg" }, + Environment = new EnvVar[] + { + new EnvVar { Name = "var1", Value = "value1"}, + new EnvVar { Name = "var2", Value = "value2"}, + }, + LaunchConfigurations = new LaunchConfiguration[] + { + new() { + ProjectPath = @"c:\test\Projects\project1.csproj", + LaunchType = RunSessionRequest.ProjectLaunchConfigurationType, + LaunchMode= RunSessionRequest.DebugLaunchMode, + LaunchProfile = "specificProfileName", + DisableLaunchProfile = true + } + } + }; + + var projectReq = runSessionReq.ToProjectLaunchInformation(); + + Assert.Equal(runSessionReq.Arguments[0], projectReq.Arguments.First()); + Assert.Equal(runSessionReq.Environment.Length, projectReq.Environment.Count()); + Assert.Equal(runSessionReq.Environment[0].Name, projectReq.Environment.First().Key); + Assert.Equal(runSessionReq.Environment[0].Value, projectReq.Environment.First().Value); + Assert.Equal(runSessionReq.LaunchConfigurations[0].ProjectPath, projectReq.ProjectPath); + Assert.True(projectReq.Debug); + Assert.Equal(runSessionReq.LaunchConfigurations[0].LaunchProfile, projectReq.LaunchProfile); + Assert.Equal(runSessionReq.LaunchConfigurations[0].DisableLaunchProfile, projectReq.DisableLaunchProfile); + } +} diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/App.csproj b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/App.csproj new file mode 100644 index 000000000000..4505e48191a6 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/App.csproj @@ -0,0 +1,13 @@ + + + + Exe + $(CurrentTargetFramework) + enable + + + + + + + diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Program.cs b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Program.cs new file mode 100644 index 000000000000..d81245d92af9 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Program.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Reflection.Metadata; + +[assembly: MetadataUpdateHandler(typeof(UpdateHandler))] + +// delete the dependency dll to cause load failure of DepSubType +var depPath = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location!)!, "Dep2.dll"); +File.Delete(depPath); +Console.WriteLine($"File deleted: {depPath}"); + +while (true) +{ + lock (UpdateHandler.Guard) + { + Printer.Print(); + Dep.DepLib.F(); + } + + Thread.Sleep(100); +} + +static class UpdateHandler +{ + // Lock to avoid the updated Print method executing concurrently with the update handler. + public static object Guard = new object(); + + public static void UpdateApplication(Type[] types) + { + lock (Guard) + { + Console.WriteLine($"Updated types: {(types == null ? "" : types.Length == 0 ? "" : string.Join(",", types.Select(t => t.Name)))}"); + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Update.cs b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Update.cs new file mode 100644 index 000000000000..9ef906a439f0 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/App/Update.cs @@ -0,0 +1,12 @@ +using System; + +public class DepType +{ + int F() => 1; +} + +public class Printer +{ + public static void Print() + => Console.WriteLine("Hello!"); +} diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.cs b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.cs new file mode 100644 index 000000000000..404a726ae4f4 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.cs @@ -0,0 +1,13 @@ +// This attribute is not causing Dep.dll to be loaded, but enough +// to cause the HotReloadAgent to fail on getting custom attributes. +[assembly: Dep2.Test()] + +namespace Dep; + +public class DepLib +{ + public static void F() + { + Console.WriteLine(1); + } +} diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.csproj b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.csproj new file mode 100644 index 000000000000..2776c7abc57d --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep/Dep.csproj @@ -0,0 +1,12 @@ + + + + $(CurrentTargetFramework) + enable + + + + + + + diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.cs b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.cs new file mode 100644 index 000000000000..548f61133dc7 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.cs @@ -0,0 +1,9 @@ +namespace Dep2; + +public class Dep2Lib +{ + void F() + { + Console.WriteLine(1); + } +} diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.csproj b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.csproj new file mode 100644 index 000000000000..78589723776a --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/Dep2.csproj @@ -0,0 +1,8 @@ + + + + $(CurrentTargetFramework) + enable + + + diff --git a/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/TestAttribute.cs b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/TestAttribute.cs new file mode 100644 index 000000000000..c2f2a9b52c12 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAppMissingAssemblyFailure/Dep2/TestAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Dep2; + +[AttributeUsage(AttributeTargets.Assembly)] +public class TestAttribute : Attribute +{ +} diff --git a/test/dotnet-MsiInstallation.Tests/Framework/TestExtensions.cs b/test/dotnet-MsiInstallation.Tests/Framework/TestExtensions.cs new file mode 100644 index 000000000000..d9acc9a5e945 --- /dev/null +++ b/test/dotnet-MsiInstallation.Tests/Framework/TestExtensions.cs @@ -0,0 +1,21 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.MsiInstallerTests.Framework +{ + public static class TestExtensions + { + public static AndConstraint PassWithoutWarning(this CommandResultAssertions assertions) + { + return assertions.Pass() + .And.NotHaveStdOutContaining("Warning: ") + .And.NotHaveStdErrContaining("Warning: "); + } + } +} diff --git a/test/dotnet-MsiInstallation.Tests/MsiInstallerTests.cs b/test/dotnet-MsiInstallation.Tests/MsiInstallerTests.cs index 213e5deb43db..ccc89980d6cb 100644 --- a/test/dotnet-MsiInstallation.Tests/MsiInstallerTests.cs +++ b/test/dotnet-MsiInstallation.Tests/MsiInstallerTests.cs @@ -54,7 +54,7 @@ private CommandResult ApplyManifests(string manifestContents, string rollbackID) VM.WriteFile($@"c:\SdkTesting\rollback-{rollbackID}.json", manifestContents), VM.CreateRunCommand("dotnet", "workload", "update", "--from-rollback-file", $@"c:\SdkTesting\rollback-{rollbackID}.json", "--skip-sign-check")) .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); return result; } @@ -213,10 +213,10 @@ public void InstallWithRollback() InstallSdk(); VM.WriteFile($@"c:\SdkTesting\rollback-rc1.json", RollbackRC1) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); VM.CreateRunCommand("dotnet", "workload", "install", "wasm-tools", "--from-rollback-file", $@"c:\SdkTesting\rollback-rc1.json") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); TestWasmWorkload(); } @@ -242,7 +242,7 @@ public void UpdateShouldUndoPinnedRollback() VM.CreateRunCommand("dotnet", "workload", "update") .Execute() - .Should().Pass(); + .Should().PassWithoutWarning(); GetWorkloadVersion().Should().NotBe(workloadVersion); @@ -269,7 +269,7 @@ public void TestAspire() //AddNuGetSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-aspire-d215c528/nuget/v3/index.json"); //VM.CreateRunCommand("powershell", "-Command", "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }") - // .Execute().Should().Pass(); + // .Execute().Should().PassWithoutWarning(); InstallWorkload("aspire", skipManifestUpdate: true); @@ -277,7 +277,7 @@ public void TestAspire() .WithWorkingDirectory(@"c:\SdkTesting") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); } @@ -289,13 +289,13 @@ void TestWasmWorkload() .WithWorkingDirectory(@"c:\SdkTesting") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); var result = VM.CreateRunCommand("dotnet", "publish", "/p:RunAotCompilation=true") .WithWorkingDirectory(@"c:\SdkTesting\BlazorWasm") .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); // When publishing a blazorwasm project without the wasm-tools workload installed, the following message is displayed: // Publishing without optimizations. Although it's optional for Blazor, we strongly recommend using `wasm-tools` workload! You can install it by running `dotnet workload install wasm-tools` from the command line. @@ -364,7 +364,7 @@ string ListWorkloads() .WithIsReadOnly(true) .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); return result.StdOut; } diff --git a/test/dotnet-MsiInstallation.Tests/VSWorkloadTests.cs b/test/dotnet-MsiInstallation.Tests/VSWorkloadTests.cs index 7d28064c8002..5aae507aef0a 100644 --- a/test/dotnet-MsiInstallation.Tests/VSWorkloadTests.cs +++ b/test/dotnet-MsiInstallation.Tests/VSWorkloadTests.cs @@ -25,7 +25,7 @@ public void WorkloadListShowsVSInstalledWorkloads() .WithIsReadOnly(true) .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); result.Should().HaveStdOutContaining("aspire"); } @@ -39,12 +39,12 @@ public void UpdatesAreAdvertisedForVSInstalledWorkloads() .WithWorkingDirectory(@"C:\SdkTesting") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); // build (or any restoring) command should check for and notify of updates VM.CreateRunCommand("dotnet", "build") .WithWorkingDirectory(@"C:\SdkTesting\LibraryTest") - .Execute().Should().Pass() + .Execute().Should().PassWithoutWarning() .And.HaveStdOutContaining("Workload updates are available"); // Workload list should list the specific workloads that have updates @@ -52,7 +52,7 @@ public void UpdatesAreAdvertisedForVSInstalledWorkloads() .WithIsReadOnly(true) .Execute() .Should() - .Pass() + .PassWithoutWarning() .And .HaveStdOutContaining("Updates are available for the following workload(s): aspire"); } diff --git a/test/dotnet-MsiInstallation.Tests/WorkloadSetTests.cs b/test/dotnet-MsiInstallation.Tests/WorkloadSetTests.cs index 06152247f5bd..e7d79cede1d8 100644 --- a/test/dotnet-MsiInstallation.Tests/WorkloadSetTests.cs +++ b/test/dotnet-MsiInstallation.Tests/WorkloadSetTests.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.DotNet.MsiInstallerTests.Framework; using Microsoft.NET.Sdk.WorkloadManifestReader; @@ -47,7 +49,7 @@ public void DoesNotUseWorkloadSetsByDefault() VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); var originalRollback = GetRollback(); @@ -56,7 +58,7 @@ public void DoesNotUseWorkloadSetsByDefault() VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); var newRollback = GetRollback(); @@ -73,7 +75,7 @@ void UpdateAndSwitchToWorkloadSetMode(out string updatedWorkloadVersion, out Wor VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); rollbackAfterUpdate = GetRollback(); updatedWorkloadVersion = GetWorkloadVersion(); @@ -86,10 +88,17 @@ void UpdateAndSwitchToWorkloadSetMode(out string updatedWorkloadVersion, out Wor .WithDescription("Switch mode to workload-set") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(updatedWorkloadVersion); + var expectedMessage = "Workloads are configured to install and update using workload versions, but none were found. Run \"dotnet workload restore\" to install a workload version."; + + GetDotnetInfo().Should().Contain(expectedMessage) + .And.NotContain("(not installed)"); + GetDotnetWorkloadInfo().Should().Contain(expectedMessage) + .And.NotContain("(not installed)"); + GetUpdateMode().Should().Be("workload-set"); } @@ -103,7 +112,7 @@ public void UpdateWithWorkloadSets() AddNuGetSource(@"c:\SdkTesting\WorkloadSets"); VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion2); @@ -122,7 +131,7 @@ public void UpdateInWorkloadSetModeWithNoAvailableWorkloadSet() VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews", "--source", @"c:\SdkTesting\EmptySource") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); var newRollback = GetRollback(); @@ -155,13 +164,13 @@ private void UpdateToWorkloadSetVersion(string versionToInstall) VM.CreateRunCommand("dotnet", "workload", "update", "--version", versionToInstall, "--include-previews") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); VM.CreateRunCommand("dotnet", "workload", "search") .WithIsReadOnly(true) .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(versionToInstall); @@ -173,7 +182,7 @@ private void UpdateToWorkloadSetVersion(string versionToInstall) VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion2); } @@ -198,7 +207,7 @@ public void UpdateToUnavailableWorkloadSetVersion() .WithIsReadOnly(true) .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(workloadVersionBeforeUpdate); } @@ -222,7 +231,7 @@ public void UpdateWorkloadSetWithoutAvailableManifests() .WithIsReadOnly(true) .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(workloadVersionBeforeUpdate); } @@ -243,7 +252,7 @@ public void UpdateToWorkloadSetVersionWithManifestsNotAvailable() .WithIsReadOnly(true) .Execute() .Should() - .Pass(); + .PassWithoutWarning(); GetWorkloadVersion().Should().Be(workloadVersionBeforeUpdate); } @@ -258,9 +267,13 @@ void SetupWorkloadSetInGlobalJson(out WorkloadSet originalRollback) originalRollback = GetRollback(SdkTestingDirectory); - VM.WriteFile("C:\\SdkTesting\\global.json", @$"{{""sdk"":{{""workloadVersion"":""{versionToUpdateTo}""}}}}").Execute().Should().Pass(); + VM.WriteFile("C:\\SdkTesting\\global.json", @$"{{""sdk"":{{""workloadVersion"":""{versionToUpdateTo}""}}}}").Execute().Should().PassWithoutWarning(); - GetWorkloadVersion(SdkTestingDirectory).Should().Be(versionToUpdateTo); + GetWorkloadVersion(SdkTestingDirectory).Should().Be(versionToUpdateTo + " (not installed)"); + GetDotnetInfo(SdkTestingDirectory).Should().Contain("Workload version: " + versionToUpdateTo + " (not installed)") + .And.Contain($@"Workload version {versionToUpdateTo}, which was specified in C:\SdkTesting\global.json, was not found"); + GetDotnetWorkloadInfo(SdkTestingDirectory).Should().Contain("Workload version: " + versionToUpdateTo + " (not installed)") + .And.Contain($@"Workload version {versionToUpdateTo}, which was specified in C:\SdkTesting\global.json, was not found"); // The version should have changed but not yet the manifests. Since we expect both, getting the rollback should fail. var result = VM.CreateRunCommand("dotnet", "workload", "update", "--print-rollback") @@ -283,13 +296,13 @@ public void RestoreWorkloadSetViaGlobalJson() var testProjectFolder = Path.Combine(SdkTestingDirectory, "ConsoleApp"); VM.CreateRunCommand("dotnet", "new", "console", "-o", "ConsoleApp") .WithWorkingDirectory(SdkTestingDirectory) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); SetupWorkloadSetInGlobalJson(out var originalRollback); VM.CreateRunCommand("dotnet", "workload", "restore") .WithWorkingDirectory(testProjectFolder) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion(SdkTestingDirectory).Should().Be(WorkloadSetVersion2); @@ -304,7 +317,7 @@ public void UseGlobalJsonToSpecifyWorkloadSet(string command) SetupWorkloadSetInGlobalJson(out var originalRollback); string[] args = command.Equals("install") ? ["dotnet", "workload", "install", "aspire"] : ["dotnet", "workload", command]; - VM.CreateRunCommand(args).WithWorkingDirectory(SdkTestingDirectory).Execute().Should().Pass(); + VM.CreateRunCommand(args).WithWorkingDirectory(SdkTestingDirectory).Execute().Should().PassWithoutWarning(); GetRollback(SdkTestingDirectory).Should().NotBe(originalRollback); } @@ -342,12 +355,12 @@ public void InstallWithVersionWhenPinned() originalVersion.Should().NotBe(WorkloadSetVersion1); VM.CreateRunCommand("dotnet", "workload", "update", "--version", WorkloadSetVersion1, "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion1); VM.CreateRunCommand("dotnet", "workload", "install", "aspire", "--version", WorkloadSetVersion2) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion2); } @@ -363,13 +376,13 @@ public void InstallWithGlobalJsonWhenPinned() originalVersion.Should().NotBe(WorkloadSetVersion1); VM.CreateRunCommand("dotnet", "workload", "update", "--version", WorkloadSetVersion1, "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion1); VM.CreateRunCommand("dotnet", "workload", "install", "aspire") .WithWorkingDirectory(SdkTestingDirectory) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion(SdkTestingDirectory).Should().Be(WorkloadSetVersion2); @@ -391,10 +404,10 @@ public void UpdateShouldNotPinWorkloadSet() VM.CreateActionGroup($"Disable {WorkloadSetVersion2}", VM.CreateRunCommand("cmd", "/c", "ren", @$"c:\SdkTesting\WorkloadSets\Microsoft.NET.Workloads.{sdkFeatureBand}.{packageVersion}.nupkg", $"Microsoft.NET.Workloads.{sdkFeatureBand}.{packageVersion}.bak"), VM.CreateRunCommand("cmd", "/c", "ren", @$"c:\SdkTesting\WorkloadSets\Microsoft.NET.Workloads.{sdkFeatureBand}.*.{packageVersion}.nupkg", $"Microsoft.NET.Workloads.{sdkFeatureBand}.*.{packageVersion}.bak")) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion1); @@ -402,7 +415,7 @@ public void UpdateShouldNotPinWorkloadSet() VM.CreateActionGroup($"Enable {WorkloadSetVersion2}", VM.CreateRunCommand("cmd", "/c", "ren", @$"c:\SdkTesting\WorkloadSets\Microsoft.NET.Workloads.{sdkFeatureBand}.{packageVersion}.bak", $"Microsoft.NET.Workloads.{sdkFeatureBand}.{packageVersion}.nupkg"), VM.CreateRunCommand("cmd", "/c", "ren", @$"c:\SdkTesting\WorkloadSets\Microsoft.NET.Workloads.{sdkFeatureBand}.*.{packageVersion}.bak", $"Microsoft.NET.Workloads.{sdkFeatureBand}.*.{packageVersion}.nupkg")) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); InstallWorkload("aspire", skipManifestUpdate: false); @@ -436,7 +449,7 @@ public void GarbageCollectWorkloadSets() // Update to latest workload set version VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); GetWorkloadVersion().Should().Be(WorkloadSetVersion2); @@ -449,22 +462,22 @@ public void GarbageCollectWorkloadSets() // Downgrade to earlier workload set version VM.CreateRunCommand("dotnet", "workload", "update", "--version", WorkloadSetVersion1) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); // Later workload set version should be GC'd VM.GetRemoteDirectory(workloadSet2Path).Should().NotExist(); // Now, pin older workload set version in global.json - VM.WriteFile("C:\\SdkTesting\\global.json", @$"{{""sdk"":{{""workloadVersion"":""{WorkloadSetVersion1}""}}}}").Execute().Should().Pass(); + VM.WriteFile("C:\\SdkTesting\\global.json", @$"{{""sdk"":{{""workloadVersion"":""{WorkloadSetVersion1}""}}}}").Execute().Should().PassWithoutWarning(); // Install pinned version VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") .WithWorkingDirectory(SdkTestingDirectory) - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); // Update globally installed version to later version VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); // Check workload versions in global context and global.json directory GetWorkloadVersion().Should().Be(WorkloadSetVersion2); @@ -475,11 +488,11 @@ public void GarbageCollectWorkloadSets() VM.GetRemoteDirectory(workloadSet1Path).Should().Exist(); // Now, remove pinned workload set from global.json - VM.WriteFile("C:\\SdkTesting\\global.json", "{}").Execute().Should().Pass(); + VM.WriteFile("C:\\SdkTesting\\global.json", "{}").Execute().Should().PassWithoutWarning(); // Run workload update to do a GC VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews") - .Execute().Should().Pass(); + .Execute().Should().PassWithoutWarning(); // Workload set 1 should have been GC'd VM.GetRemoteDirectory(workloadSet1Path).Should().NotExist(); @@ -502,6 +515,53 @@ public void FinalizerUninstallsWorkloadSets() VM.GetRemoteDirectory(workloadSetPath).Should().NotExist(); } + [Fact] + public void WorkloadSearchVersion() + { + InstallSdk(); + + // Run `dotnet workload search version` without source set up + var searchVersionResult = VM.CreateRunCommand("dotnet", "workload", "search", "version") + .WithIsReadOnly(true) + .Execute(); ; + searchVersionResult.Should().PassWithoutWarning(); + + // Without source set up, there should be no workload sets found + searchVersionResult.StdOut.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Should().BeEmpty(); + searchVersionResult.StdErr.Should().Contain($"No workload versions found for SDK feature band {new SdkFeatureBand(SdkInstallerVersion)}"); + + // Add source so workload sets will be found + AddNuGetSource(@"c:\SdkTesting\WorkloadSets"); + + // `dotnet workload search version` should return expected versions + searchVersionResult = VM.CreateRunCommand("dotnet", "workload", "search", "version") + .WithIsReadOnly(true) + .Execute(); + searchVersionResult.Should().PassWithoutWarning(); + var actualVersions = searchVersionResult.StdOut.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + actualVersions.Should().Equal(WorkloadSetVersion2, WorkloadSetVersion1); + + + // `dotnet workload search version --format json` should return manifest versions (and eventually perhaps other information) about that workload set + searchVersionResult = VM.CreateRunCommand("dotnet", "workload", "search", "version", WorkloadSetVersion2, "--format", "json") + .WithIsReadOnly(true) + .Execute(); + + searchVersionResult.Should().PassWithoutWarning(); + + var searchResultJson = JsonNode.Parse(searchVersionResult.StdOut); + var searchResultWorkloadSet = WorkloadSet.FromDictionaryForJson(JsonSerializer.Deserialize>(searchResultJson["manifestVersions"]), new SdkFeatureBand(SdkInstallerVersion)); + + // Update to the workload set version we got the search info from so we can check to see if the manifest versions match what we expect + VM.CreateRunCommand("dotnet", "workload", "update", "--include-previews", "--version", WorkloadSetVersion2) + .Execute() + .Should() + .PassWithoutWarning(); + + GetRollback().ManifestVersions.Should().BeEquivalentTo(searchResultWorkloadSet.ManifestVersions); + } + string GetWorkloadVersion(string workingDirectory = null) { var result = VM.CreateRunCommand("dotnet", "workload", "--version") @@ -509,17 +569,42 @@ string GetWorkloadVersion(string workingDirectory = null) .WithIsReadOnly(true) .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); + + return result.StdOut; + } + + string GetDotnetInfo(string workingDirectory = null) + { + var result = VM.CreateRunCommand("dotnet", "--info") + .WithWorkingDirectory(workingDirectory) + .WithIsReadOnly(true) + .Execute(); + + result.Should().PassWithoutWarning(); return result.StdOut; } + + string GetDotnetWorkloadInfo(string workingDirectory = null) + { + var result = VM.CreateRunCommand("dotnet", "workload", "--info") + .WithWorkingDirectory(workingDirectory) + .WithIsReadOnly(true) + .Execute(); + + result.Should().PassWithoutWarning(); + + return result.StdOut; + } + string GetUpdateMode() { var result = VM.CreateRunCommand("dotnet", "workload", "config", "--update-mode") .WithIsReadOnly(true) .Execute(); - result.Should().Pass(); + result.Should().PassWithoutWarning(); return result.StdOut; } @@ -531,7 +616,7 @@ void AddNuGetSource(string source, string directory = null) .WithDescription($"Add {source} to NuGet.config") .Execute() .Should() - .Pass(); + .PassWithoutWarning(); } } } diff --git a/test/dotnet-new.Tests/CommonTemplatesTests.cs b/test/dotnet-new.Tests/CommonTemplatesTests.cs index 6f598250a7ef..98c86dde4016 100644 --- a/test/dotnet-new.Tests/CommonTemplatesTests.cs +++ b/test/dotnet-new.Tests/CommonTemplatesTests.cs @@ -230,8 +230,8 @@ public async Task AotVariants(string name, string language) var templatesToTest = new[] { - new { Template = consoleTemplateShortname, Frameworks = new[] { null, "net6.0", "net7.0", "net8.0" } }, - new { Template = "classlib", Frameworks = new[] { null, "net6.0", "net7.0", "net8.0", "netstandard2.0", "netstandard2.1" } } + new { Template = consoleTemplateShortname, Frameworks = new[] { null, "net6.0", "net8.0" } }, + new { Template = "classlib", Frameworks = new[] { null, "net6.0", "net8.0", "netstandard2.0", "netstandard2.1" } } }; //features: top-level statements; nullables; implicit usings; filescoped namespaces @@ -240,9 +240,9 @@ public async Task AotVariants(string name, string language) //C# 12 is not supported yet - https://github.com/dotnet/sdk/issues/29195 string?[] supportedLanguageVersions = { null, "ISO-2", "2", "3", "4", "5", "6", "7", "7.1", "7.2", "7.3", "8.0", "9.0", "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" }; - string?[] nullableSupportedInFrameworkByDefault = { null, "net6.0", "net7.0", "net8.0", "netstandard2.1" }; - string?[] implicitUsingsSupportedInFramework = { null, "net6.0", "net7.0", "net8.0" }; - string?[] fileScopedNamespacesSupportedFrameworkByDefault = { null, "net6.0", "net7.0", "net8.0" }; + string?[] nullableSupportedInFrameworkByDefault = { null, "net6.0", "net8.0", "netstandard2.1" }; + string?[] implicitUsingsSupportedInFramework = { null, "net6.0", "net8.0" }; + string?[] fileScopedNamespacesSupportedFrameworkByDefault = { null, "net6.0", "net8.0" }; string?[] nullableSupportedLanguages = { "8.0", "9.0", "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" }; string?[] topLevelStatementSupportedLanguages = { null, "9.0", "10.0", "11", "11.0", /*"12",*/ "latest", "latestMajor", "default", "preview" }; diff --git a/test/dotnet-watch.Tests/FileWatcherTests.cs b/test/dotnet-watch.Tests/FileWatcherTests.cs index 03afa158fb79..eeadd08ceefd 100644 --- a/test/dotnet-watch.Tests/FileWatcherTests.cs +++ b/test/dotnet-watch.Tests/FileWatcherTests.cs @@ -1,53 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Testing; using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.DotNet.Watcher.Tools { - public class FileWatcherTests + public class FileWatcherTests(ITestOutputHelper output) { - public FileWatcherTests(ITestOutputHelper output) - { - _output = output; - _testAssetManager = new TestAssetsManager(output); - } - private readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); private readonly TimeSpan NegativeTimeout = TimeSpan.FromSeconds(5); - private readonly ITestOutputHelper _output; - private readonly TestAssetsManager _testAssetManager; + private readonly TestAssetsManager _testAssetManager = new TestAssetsManager(output); private async Task TestOperation( string dir, - (string, bool)[] expectedChanges, + (string path, ChangeKind kind)[] expectedChanges, bool usePolling, Action operation) { - // On Unix the native file watcher may surface events from - // the recent past. Delay to avoid those. - // On Unix the file write time is in 1s increments; - // if we don't wait, there's a chance that the polling - // watcher will not detect the change - await Task.Delay(1250); - using var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling); if (watcher is DotnetFileWatcher dotnetWatcher) { - dotnetWatcher.Logger = m => _output.WriteLine(m); + dotnetWatcher.Logger = m => output.WriteLine(m); } var changedEv = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var filesChanged = new HashSet<(string, bool)>(); + var filesChanged = new HashSet<(string path, ChangeKind kind)>(); - var testFileFullPath = Path.Combine(dir, "foo"); - - EventHandler<(string path, bool newFile)> handler = null; + EventHandler<(string path, ChangeKind kind)> handler = null; handler = (_, f) => { - filesChanged.Add(f); + if (filesChanged.Add(f)) + { + output.WriteLine($"Observed new {f.kind}: '{f.path}' ({filesChanged.Count} out of {expectedChanges.Length})"); + } + else + { + output.WriteLine($"Already seen {f.kind}: '{f.path}'"); + } if (filesChanged.Count == expectedChanges.Length) { @@ -88,17 +78,48 @@ await TestOperation( expectedChanges: !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !usePolling ? new[] { - (testFileFullPath, false), - (testFileFullPath, true), + (testFileFullPath, ChangeKind.Update), + (testFileFullPath, ChangeKind.Add), } : new[] { - (testFileFullPath, true), + (testFileFullPath, ChangeKind.Add), }, usePolling, () => File.WriteAllText(testFileFullPath, string.Empty)); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NewFileInNewDirectory(bool usePolling) + { + var dir = _testAssetManager.CreateTestDirectory(identifier: usePolling.ToString()).Path; + + var newDir = Path.Combine(dir, "Dir"); + var newFile = Path.Combine(newDir, "foo"); + + await TestOperation( + dir, + expectedChanges: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !usePolling + ? new[] + { + (newDir, ChangeKind.Add), + (newFile, ChangeKind.Update), + (newFile, ChangeKind.Add), + } + : new[] + { + (newDir, ChangeKind.Add), + }, + usePolling, + () => + { + Directory.CreateDirectory(newDir); + File.WriteAllText(newFile, string.Empty); + }); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -111,14 +132,13 @@ public async Task ChangeFile(bool usePolling) await TestOperation( dir, - expectedChanges: [(testFileFullPath, false)], + expectedChanges: [(testFileFullPath, ChangeKind.Update)], usePolling, () => File.WriteAllText(testFileFullPath, string.Empty)); } [Theory] - [InlineData(true)] - [InlineData(false)] + [CombinatorialData] public async Task MoveFile(bool usePolling) { var dir = _testAssetManager.CreateTestDirectory(identifier: usePolling.ToString()).Path; @@ -129,18 +149,19 @@ public async Task MoveFile(bool usePolling) await TestOperation( dir, - expectedChanges: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !usePolling - ? new[] - { - (srcFile, false), - (srcFile, true), - (dstFile, true), - } - : new[] - { - (srcFile, false), - (dstFile, true), - }, + expectedChanges: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !usePolling ? + [ + // On OSX events from before we started observing are reported as well. + (srcFile, ChangeKind.Update), + (srcFile, ChangeKind.Add), + (srcFile, ChangeKind.Delete), + (dstFile, ChangeKind.Add), + ] + : + [ + (srcFile, ChangeKind.Delete), + (dstFile, ChangeKind.Add), + ], usePolling, () => File.Move(srcFile, dstFile)); @@ -160,8 +181,8 @@ public async Task FileInSubdirectory() await TestOperation( dir, expectedChanges: [ - (subdir, false), - (testFileFullPath, false) + (subdir, ChangeKind.Update), + (testFileFullPath, ChangeKind.Update) ], usePolling: true, () => File.WriteAllText(testFileFullPath, string.Empty)); @@ -246,7 +267,7 @@ public async Task MultipleFiles(bool usePolling) await TestOperation( dir, - expectedChanges: [(testFileFullPath, false)], + expectedChanges: [(testFileFullPath, ChangeKind.Update)], usePolling: true, () => File.WriteAllText(testFileFullPath, string.Empty)); } @@ -274,9 +295,9 @@ private async Task AssertFileChangeRaisesEvent(string directory, IFileSystemWatc { var changedEv = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var expectedPath = Path.Combine(directory, Path.GetRandomFileName()); - EventHandler<(string, bool)> handler = (_, f) => + EventHandler<(string, ChangeKind)> handler = (_, f) => { - _output.WriteLine("File changed: " + f); + output.WriteLine("File changed: " + f); try { if (string.Equals(f.Item1, expectedPath, StringComparison.OrdinalIgnoreCase)) @@ -331,13 +352,43 @@ public async Task DeleteSubfolder(bool usePolling) await TestOperation( dir, - expectedChanges: [ - (subdir, false), - (f1, false), - (f2, false), - (f3, false), + expectedChanges: usePolling ? + [ + (subdir, ChangeKind.Delete), + (f1, ChangeKind.Delete), + (f2, ChangeKind.Delete), + (f3, ChangeKind.Delete), + ] + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + [ + (subdir, ChangeKind.Add), + (subdir, ChangeKind.Delete), + (f1, ChangeKind.Update), + (f1, ChangeKind.Add), + (f1, ChangeKind.Delete), + (f2, ChangeKind.Update), + (f2, ChangeKind.Add), + (f2, ChangeKind.Delete), + (f3, ChangeKind.Update), + (f3, ChangeKind.Add), + (f3, ChangeKind.Delete), + ] + : RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + [ + (subdir, ChangeKind.Update), + (subdir, ChangeKind.Delete), + (f1, ChangeKind.Delete), + (f2, ChangeKind.Delete), + (f3, ChangeKind.Delete), + ] + : + [ + (subdir, ChangeKind.Delete), + (f1, ChangeKind.Delete), + (f2, ChangeKind.Delete), + (f3, ChangeKind.Delete), ], - usePolling: true, + usePolling, () => Directory.Delete(subdir, recursive: true)); } } diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index 45a6245c7452..afe722d75493 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -120,5 +120,149 @@ public async Task BlazorWasm() //UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource); //await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded); } + + // Test is timing out on .NET Framework: https://github.com/dotnet/sdk/issues/41669 + [CoreMSBuildOnlyFact] + public async Task HandleMissingAssemblyFailure() + { + var testAsset = TestAssets.CopyTestAsset("WatchAppMissingAssemblyFailure") + .WithSource(); + + App.Start(testAsset, [], "App"); + + await App.AssertWaitingForChanges(); + + var newSrc = /* lang=c#-test */""" + using System; + + public class DepType + { + int F() => 1; + } + + public class Printer + { + public static void Print() + => Console.WriteLine("Updated!"); + } + """; + + // Delete all files in testAsset.Path named Dep.dll + foreach (var depDll in Directory.GetFiles(testAsset.Path, "Dep2.dll", SearchOption.AllDirectories)) + { + File.Delete(depDll); + } + + File.WriteAllText(Path.Combine(testAsset.Path, "App", "Update.cs"), newSrc); + + await App.AssertOutputLineStartsWith("Updated types: Printer"); + } + + [Theory] + [InlineData(true, Skip = "https://github.com/dotnet/sdk/issues/43320")] + [InlineData(false)] + public async Task RenameSourceFile(bool useMove) + { + Logger.WriteLine("RenameSourceFile started"); + + var testAsset = TestAssets.CopyTestAsset("WatchAppWithProjectDeps") + .WithSource(); + + var dependencyDir = Path.Combine(testAsset.Path, "Dependency"); + var oldFilePath = Path.Combine(dependencyDir, "Foo.cs"); + var newFilePath = Path.Combine(dependencyDir, "Renamed.cs"); + + var source = """ + using System; + using System.IO; + using System.Runtime.CompilerServices; + + public class Lib + { + public static void Print() => PrintFileName(); + + public static void PrintFileName([CallerFilePathAttribute] string filePath = null) + { + Console.WriteLine($"> {Path.GetFileName(filePath)}"); + } + } + """; + + File.WriteAllText(oldFilePath, source); + + App.Start(testAsset, [], "AppWithDeps"); + + await App.AssertWaitingForChanges(); + + // rename the file: + if (useMove) + { + File.Move(oldFilePath, newFilePath); + } + else + { + File.Delete(oldFilePath); + File.WriteAllText(newFilePath, source); + } + + Logger.WriteLine($"Renamed '{oldFilePath}' to '{newFilePath}'."); + + await App.AssertOutputLineStartsWith("> Renamed.cs"); + } + + [Theory] + [InlineData(true, Skip = "https://github.com/dotnet/sdk/issues/43320")] + [InlineData(false)] + public async Task RenameDirectory(bool useMove) + { + Logger.WriteLine("RenameSourceFile started"); + + var testAsset = TestAssets.CopyTestAsset("WatchAppWithProjectDeps") + .WithSource(); + + var dependencyDir = Path.Combine(testAsset.Path, "Dependency"); + var oldSubdir = Path.Combine(dependencyDir, "Subdir"); + var newSubdir = Path.Combine(dependencyDir, "NewSubdir"); + + var source = """ + using System; + using System.IO; + using System.Runtime.CompilerServices; + + public class Lib + { + public static void Print() => PrintDirectoryName(); + + public static void PrintDirectoryName([CallerFilePathAttribute] string filePath = null) + { + Console.WriteLine($"> {Path.GetFileName(Path.GetDirectoryName(filePath))}"); + } + } + """; + + File.Delete(Path.Combine(dependencyDir, "Foo.cs")); + Directory.CreateDirectory(oldSubdir); + File.WriteAllText(Path.Combine(oldSubdir, "Foo.cs"), source); + + App.Start(testAsset, [], "AppWithDeps"); + + await App.AssertWaitingForChanges(); + + // rename the directory: + if (useMove) + { + Directory.Move(oldSubdir, newSubdir); + } + else + { + Directory.Delete(oldSubdir, recursive: true); + Directory.CreateDirectory(newSubdir); + File.WriteAllText(Path.Combine(newSubdir, "Foo.cs"), source); + } + + Logger.WriteLine($"Renamed '{oldSubdir}' to '{newSubdir}'."); + + await App.AssertOutputLineStartsWith("> NewSubdir"); + } } } diff --git a/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs b/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs index 0a64ca3f0c33..6b0aa27c851a 100644 --- a/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs +++ b/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools @@ -27,7 +28,7 @@ public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges() evaluator.RequiresRevaluation = false; - await evaluator.EvaluateAsync(changedFile: new() { FilePath = "Test.csproj" }, CancellationToken.None); + await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Test.csproj" }, ChangeKind.Update), CancellationToken.None); Assert.True(evaluator.RequiresRevaluation); } @@ -51,7 +52,7 @@ public async Task ProcessAsync_DoesNotEvaluateFileSetIfNonProjFileChanges() evaluator.RequiresRevaluation = false; - await evaluator.EvaluateAsync(changedFile: new() { FilePath = "Controller.cs" }, CancellationToken.None); + await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None); Assert.False(evaluator.RequiresRevaluation); Assert.Equal(1, counter); @@ -77,7 +78,7 @@ public async Task ProcessAsync_EvaluateFileSetOnEveryChangeIfOptimizationIsSuppr evaluator.RequiresRevaluation = false; - await evaluator.EvaluateAsync(changedFile: new() { FilePath = "Controller.cs" }, CancellationToken.None); + await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None); Assert.True(evaluator.RequiresRevaluation); Assert.Equal(2, counter); @@ -118,7 +119,7 @@ public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIs evaluator.RequiresRevaluation = false; evaluator.Timestamps["Proj.csproj"] = new DateTime(1007); - await evaluator.EvaluateAsync(new() { FilePath = "Controller.cs" }, CancellationToken.None); + await evaluator.EvaluateAsync(new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None); Assert.True(evaluator.RequiresRevaluation); } diff --git a/test/dotnet-workload-install.Tests/FailingNuGetPackageInstaller.cs b/test/dotnet-workload-install.Tests/FailingNuGetPackageInstaller.cs index e77075b2051a..a3cca4e9e9e9 100644 --- a/test/dotnet-workload-install.Tests/FailingNuGetPackageInstaller.cs +++ b/test/dotnet-workload-install.Tests/FailingNuGetPackageInstaller.cs @@ -23,7 +23,8 @@ public Task DownloadPackageAsync(PackageId packageId, NuGetVersion packa bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping packageSourceMapping = null, + bool isTool = false) { var mockPackagePath = Path.Combine(MockPackageDir, $"{packageId}.{packageVersion}.nupkg"); File.WriteAllText(mockPackagePath, string.Empty); diff --git a/test/dotnet-workload-install.Tests/MockManifestProvider.cs b/test/dotnet-workload-install.Tests/MockManifestProvider.cs index ee261164eecb..2f0c86a1b01b 100644 --- a/test/dotnet-workload-install.Tests/MockManifestProvider.cs +++ b/test/dotnet-workload-install.Tests/MockManifestProvider.cs @@ -48,10 +48,6 @@ public IEnumerable GetManifests() } public string GetSdkFeatureBand() => SdkFeatureBand.ToString(); - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion - { - Version = GetSdkFeatureBand() + ".2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo(SdkFeatureBand.ToString() + ".2"); } } diff --git a/test/dotnet-workload-install.Tests/MockNuGetPackageInstaller.cs b/test/dotnet-workload-install.Tests/MockNuGetPackageInstaller.cs index 0a3728002e03..97d394691b00 100644 --- a/test/dotnet-workload-install.Tests/MockNuGetPackageInstaller.cs +++ b/test/dotnet-workload-install.Tests/MockNuGetPackageInstaller.cs @@ -38,7 +38,8 @@ public Task DownloadPackageAsync(PackageId packageId, bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping packageSourceMapping = null, + bool isTool = false) { DownloadCallParams.Add((packageId, packageVersion, downloadFolder, packageSourceLocation)); diff --git a/test/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs b/test/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs index c06827f0812c..f8838a08cc12 100644 --- a/test/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs +++ b/test/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs @@ -119,6 +119,8 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand }); } + public WorkloadSet GetWorkloadSetContents(string workloadSetVersion) => WorkloadSet.FromJson(workloadSetContents, new SdkFeatureBand("6.0.100")); + public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) { InstallWorkloadSetCalled = true; diff --git a/test/dotnet-workload-install.Tests/WorkloadSetVersionMappingTests.cs b/test/dotnet-workload-install.Tests/WorkloadSetVersionMappingTests.cs new file mode 100644 index 000000000000..955936d961c2 --- /dev/null +++ b/test/dotnet-workload-install.Tests/WorkloadSetVersionMappingTests.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using ManifestReaderTests; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.ToolPackage; +using Microsoft.DotNet.Workloads.Workload.Install; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using NuGet.Versioning; +using static Microsoft.NET.Sdk.WorkloadManifestReader.WorkloadResolver; +using Microsoft.Extensions.EnvironmentAbstractions; +using System.Text.Json; +using Microsoft.TemplateEngine.Edge.Constraints; + +namespace Microsoft.DotNet.Cli.Workload.Install.Tests +{ + public class WorkloadSetVersionMappingTests : SdkTest + { + + public WorkloadSetVersionMappingTests(ITestOutputHelper log) : base(log) + { + } + + public static IEnumerable WorkloadVersionsData + { + get + { + return + [ + // Workload set version, feature band, package version + ["8.0.200", "8.0.200", "8.200.0"], + ["8.0.201", "8.0.200", "8.201.0"], + ["8.0.203.1", "8.0.200", "8.203.1"], + ["9.0.100-preview.2.3.4.5.6.7.8", "9.0.100-preview.2", "9.100.0-preview.2.3.4.5.6.7.8"], + ["8.0.201.1-preview", "8.0.200", "8.201.1-preview"], + ["8.0.201.1-preview.2", "8.0.200", "8.201.1-preview.2"], + + // This apparently works accidentally, since "servicing" contains "ci", which is what the SdkFeatureBand constructor check to see if it should ignore the prerelease specifier + ["8.0.201-servicing.23015", "8.0.200", "8.201.0-servicing.23015"], + ["9.0.100-preview-servicing.1.23015", "9.0.100", "9.100.0-preview-servicing.1.23015"], + + ]; + } + } + + [Theory] + [MemberData(nameof(WorkloadVersionsData))] + public void TestWorkloadSetVersionParsing(string workloadSetVersion, string expectedFeatureBand, string expectedPackageVersion) + { + string packageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out SdkFeatureBand featureBand); + + packageVersion.Should().Be(expectedPackageVersion); + featureBand.Should().Be(new SdkFeatureBand(expectedFeatureBand)); + } + + [Theory] + [MemberData(nameof(WorkloadVersionsData))] + public void TestWorkloadSetPackageVersionParsing(string expectedWorkloadSetVersion, string packageFeatureBand, string packageVersion) + { + string workloadSetVersion = WorkloadManifestUpdater.WorkloadSetPackageVersionToWorkloadSetVersion(new SdkFeatureBand(packageFeatureBand), packageVersion); + + workloadSetVersion.Should().Be(expectedWorkloadSetVersion); + } + } +} diff --git a/test/dotnet-workload-search.Tests/MockWorkloadResolver.cs b/test/dotnet-workload-search.Tests/MockWorkloadResolver.cs index 6a52b2bd791c..45120fe3d04a 100644 --- a/test/dotnet-workload-search.Tests/MockWorkloadResolver.cs +++ b/test/dotnet-workload-search.Tests/MockWorkloadResolver.cs @@ -39,11 +39,7 @@ public void RefreshWorkloadManifests() { /* noop */ } public IEnumerable GetInstalledManifests() => _installedManifests ?? throw new NotImplementedException(); public IWorkloadResolver CreateOverlayResolver(IWorkloadManifestProvider overlayManifestProvider) => throw new NotImplementedException(); public string GetSdkFeatureBand() => "12.0.400"; - public WorkloadVersion GetWorkloadVersion() => new WorkloadVersion() - { - Version = "12.0.400.2", - WorkloadInstallType = WorkloadVersion.Type.LooseManifest - }; + public IWorkloadManifestProvider.WorkloadVersionInfo GetWorkloadVersion() => new IWorkloadManifestProvider.WorkloadVersionInfo("12.0.400.2"); public IEnumerable GetUpdatedWorkloads(WorkloadResolver advertisingManifestResolver, IEnumerable installedWorkloads) => throw new NotImplementedException(); WorkloadResolver IWorkloadResolver.CreateOverlayResolver(IWorkloadManifestProvider overlayManifestProvider) => throw new NotImplementedException(); WorkloadManifest IWorkloadResolver.GetManifestFromWorkload(WorkloadId workloadId) => throw new NotImplementedException(); diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 44b132f72971..575f0f6ebd0c 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -50,12 +50,16 @@ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress( } [Fact] - public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotDevDeviceId() + public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached() { var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"]; Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"]; + + Guid.TryParse(secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid"); + secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid"); } [Fact]