diff --git a/Directory.Build.props b/Directory.Build.props index 1f331ab9f835..d04c6f49255f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -73,10 +73,20 @@ false + + + false + + enable + + + false + + diff --git a/build-duty-report-2026-01-26.md b/build-duty-report-2026-01-26.md new file mode 100644 index 000000000000..d6b0c6c25393 --- /dev/null +++ b/build-duty-report-2026-01-26.md @@ -0,0 +1,145 @@ +# SDK Build Duty Triage Report + +**Generated:** January 26, 2026 +**Repos Monitored:** dotnet/sdk, dotnet/templating, dotnet/dotnet (VMR - SDK-owned only) + +--- + +## Summary + +| Category | Count | +|----------|-------| +| 🟢 Ready to Merge | 1 | +| ⏳ Waiting/On Hold | 1 | +| 🔴 Failing/Blocked | 13 | +| 🟡 Branch Lockdown | 10 | +| 🟠 New/Pending Validation | 2 | + +--- + +## 🟢 Ready to Merge PRs (1) + +These PRs have all checks passing and are ready for merge. + +### dotnet/dotnet VMR + +| PR | Title | Target Branch | Age | +|----|-------|---------------|-----| +| [#4419](https://github.com/dotnet/dotnet/pull/4419) | [release/10.0.1xx] Source code updates from dotnet/source-build-reference-packages | release/10.0.1xx | 0d | + +--- + +## 🟠 New/Pending Validation PRs (2) + +These PRs were just created and are awaiting initial CI validation. + +### dotnet/sdk + +| PR | Title | Target Branch | Age | +|----|-------|---------------|-----| +| [#52673](https://github.com/dotnet/sdk/pull/52673) | [release/10.0.1xx] Source code updates from dotnet/dotnet | release/10.0.1xx | 0d | + +### dotnet/templating + +| PR | Title | Target Branch | Age | +|----|-------|---------------|-----| +| [#9757](https://github.com/dotnet/templating/pull/9757) | [release/10.0.1xx] Source code updates from dotnet/dotnet | release/10.0.1xx | 3d | + +--- + +## ⏳ Waiting/On Hold PRs (1) + +These PRs have passing checks but have comments indicating they should wait before merging. + +### dotnet/templating + +| PR | Title | Target Branch | Age | Reason | +|----|-------|-----------------|-----|--------| +| [#9754](https://github.com/dotnet/templating/pull/9754) | [release/10.0.3xx] Source code updates from dotnet/dotnet | release/10.0.3xx | 4d | **Will break build** - Arcade version flow issue. Waiting for Arcade 10 to flow through VMR. CC @MichaelSimons. Also linked to [Issue #51574](https://github.com/dotnet/sdk/issues/51574). | + +--- + +## 🟡 Branch Lockdown PRs (10) + +These PRs are in branches with lockdown labels and require approval to merge. + +### dotnet/sdk (8) + +| PR | Title | Target Branch | Age | +|----|-------|---------------|-----| +| [#52667](https://github.com/dotnet/sdk/pull/52667) | [release/9.0.1xx] Update dependencies from dotnet/roslyn-analyzers | release/9.0.1xx | 1d | +| [#52624](https://github.com/dotnet/sdk/pull/52624) | [release/9.0.3xx] Update dependencies from dotnet/scenario-tests | release/9.0.3xx | 4d | +| [#52606](https://github.com/dotnet/sdk/pull/52606) | [release/9.0.1xx] Update dependencies from dotnet/scenario-tests | release/9.0.1xx | 5d | +| [#52594](https://github.com/dotnet/sdk/pull/52594) | [release/9.0.3xx] Update dependencies from dotnet/msbuild | release/9.0.3xx | 5d | +| [#52592](https://github.com/dotnet/sdk/pull/52592) | [release/9.0.3xx] Update dependencies from dotnet/arcade | release/9.0.3xx | 5d | +| [#52591](https://github.com/dotnet/sdk/pull/52591) | [release/9.0.1xx] Update dependencies from dotnet/source-build-reference-packages | release/9.0.1xx | 5d | +| [#52590](https://github.com/dotnet/sdk/pull/52590) | [release/9.0.1xx] Update dependencies from dotnet/arcade | release/9.0.1xx | 5d | +| [#52530](https://github.com/dotnet/sdk/pull/52530) | Merge branch 'release/8.0.1xx' => 'release/8.0.4xx' | release/8.0.4xx | 8d | + +### dotnet/templating (2) + +| PR | Title | Target Branch | Age | +|----|-------|---------------|-----| +| [#9746](https://github.com/dotnet/templating/pull/9746) | [release/9.0.1xx] Update dependencies from dotnet/arcade | release/9.0.1xx | 5d | +| [#9744](https://github.com/dotnet/templating/pull/9744) | [release/9.0.3xx] Update dependencies from dotnet/arcade | release/9.0.3xx | 5d | + +--- + +## 🔴 Failing/Blocked PRs (13) + +PRs with pending or failing status checks. + +### dotnet/sdk (11) + +| PR | Title | Target Branch | Age | Issue | +|----|-------|---------------|-----|-------| +| [#52662](https://github.com/dotnet/sdk/pull/52662) | [release/10.0.2xx] Source code updates from dotnet/dotnet | release/10.0.2xx | 3d | ⚠️ Opposite codeflow merged - needs decision (merge/close/force) | +| [#52657](https://github.com/dotnet/sdk/pull/52657) | Merge branch 'release/10.0.1xx' => 'release/10.0.2xx' | release/10.0.2xx | 3d | ⏳ Checks pending | +| [#52653](https://github.com/dotnet/sdk/pull/52653) | [release/10.0.2xx] Update dependencies from microsoft/testfx | release/10.0.2xx | 3d | ⏳ Checks pending | +| [#52652](https://github.com/dotnet/sdk/pull/52652) | [release/10.0.1xx] Update dependencies from microsoft/testfx | release/10.0.1xx | 3d | ⏳ Checks pending | +| [#52651](https://github.com/dotnet/sdk/pull/52651) | [main] Update dependencies from microsoft/testfx | main | 3d | ⏳ Checks pending | +| [#52596](https://github.com/dotnet/sdk/pull/52596) | [main] Source code updates from dotnet/dotnet | main | 5d | ❌ ILLink analyzer error: `System.MissingMethodException` - [Issue #52599](https://github.com/dotnet/sdk/issues/52599) | +| [#52588](https://github.com/dotnet/sdk/pull/52588) | Merge branch 'release/10.0.2xx' => 'release/10.0.3xx' | release/10.0.3xx | 5d | ❌ Build error: `TagHelperCollection` not found in RazorSdk - @dotnet/razor-tooling investigating | +| [#52585](https://github.com/dotnet/sdk/pull/52585) | [release/10.0.3xx] Source code updates from dotnet/dotnet | release/10.0.3xx | 5d | ⏳ Checks pending | +| [#52523](https://github.com/dotnet/sdk/pull/52523) | [release/10.0.2xx] Source code updates from dotnet/dotnet | release/10.0.2xx | 9d | ❌ Restore error NU1603: `Microsoft.Deployment.DotNet.Releases` version mismatch | +| [#52519](https://github.com/dotnet/sdk/pull/52519) | Merge branch 'release/10.0.3xx' => 'main' | main | 9d | ❌ Test failures: `XunitMultiTFM`, `RunWithSolutionFilterAsFirstUnmatchedToken` - fix pushed, awaiting green | + +### github-actions[bot] Merge PRs (2) + +*Note: These are cross-branch merge PRs that require manual attention.* + +| Repo | PR | Title | Target Branch | Age | Issue | +|------|-----|-------|---------------|-----|-------| +| dotnet/sdk | [#52530](https://github.com/dotnet/sdk/pull/52530) | Merge 'release/8.0.1xx' => 'release/8.0.4xx' | release/8.0.4xx | 8d | Branch Lockdown | +| dotnet/sdk | [#52529](https://github.com/dotnet/sdk/pull/52529) | Merge 'release/8.0.4xx' => 'release/9.0.1xx' | release/9.0.1xx | 8d | Branch Lockdown | + +--- + +## Notes + +- **Status API shows pending for all PRs** - These repos use Azure Pipelines/GitHub Checks API which doesn't populate the Status API. Actual check status determined from comments and PR state. +- All PRs checked against authors: `dotnet-maestro[bot]`, `github-actions[bot]` (Merge PRs only) +- dotnet/dotnet VMR PRs filtered to SDK-owned: `dotnet/sdk`, `dotnet/templating`, `dotnet/deployment-tools`, `dotnet/source-build-reference-packages` + +### Total PR Count by Repo + +| Repo | Count | +|------|-------| +| dotnet/sdk | 19 | +| dotnet/templating | 4 | +| dotnet/dotnet (VMR) | 1 | +| **Total** | **24** | + +### Key Issues Blocking Multiple PRs + +| Issue | Affected PRs | Status | +|-------|--------------|--------| +| [#52599](https://github.com/dotnet/sdk/issues/52599) - ILLink analyzer MissingMethodException | #52596 | Open, @MiYanni investigating | +| [#51574](https://github.com/dotnet/sdk/issues/51574) - Arcade version flow | templating #9754 | On hold | +| RazorSdk TagHelperCollection missing | #52588 | @dotnet/razor-tooling investigating | +| Opposite codeflow merged | #52662, #52596, #52523 | Needs decision: merge/close/force | +| Branch Lockdown (9.0.x branches) | 8 SDK PRs, 2 templating PRs | Requires approval | + +--- + +*Report generated by SDK Build Duty skill* diff --git a/sdk.slnx b/sdk.slnx index 17fb1d1eef77..b3b3f8adcf8d 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -66,6 +66,8 @@ + + diff --git a/src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs b/src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs index 547764664f14..3ad6762d2d17 100644 --- a/src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs +++ b/src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs @@ -26,6 +26,7 @@ internal sealed class StartupHook && !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS() && !OperatingSystem.IsBrowser(); + private static readonly bool s_supportsPosixSignals = s_supportsConsoleColor; #if NET10_0_OR_GREATER private static PosixSignalRegistration? s_signalRegistration; @@ -125,7 +126,7 @@ private static void RegisterSignalHandlers() [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetConsoleCtrlHandler(Delegate? handler, bool add); } - else + else if (s_supportsPosixSignals) { #if NET10_0_OR_GREATER // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix. diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs index fcf541045fc6..35b99dc1e437 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs @@ -187,39 +187,28 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation } /// Cancellation token. The cancellation should trigger on process terminatation. - public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)> assets, CancellationToken cancellationToken) + public async Task ApplyStaticAssetUpdatesAsync(IEnumerable assets, CancellationToken cancellationToken) { if (browserRefreshServer != null) { - await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.relativeUrl), cancellationToken); + await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), cancellationToken); } else { var updates = new List(); - foreach (var (filePath, relativeUrl, assemblyName, isApplicationProject) in assets) + foreach (var asset in assets) { - ImmutableArray content; try { -#if NET - var blob = await File.ReadAllBytesAsync(filePath, cancellationToken); -#else - var blob = File.ReadAllBytes(filePath); -#endif - content = ImmutableCollectionsMarshal.AsImmutableArray(blob); + ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath); + updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken)); } catch (Exception e) when (e is not OperationCanceledException) { - ClientLogger.LogError("Failed to read file {FilePath}: {Message}", filePath, e.Message); + ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message); continue; } - - updates.Add(new HotReloadStaticAssetUpdate( - assemblyName: assemblyName, - relativePath: relativeUrl, - content: content, - isApplicationProject)); } await ApplyStaticAssetUpdatesAsync([.. updates], isProcessSuspended: false, cancellationToken); diff --git a/src/BuiltInTools/HotReloadClient/HotReloadStaticAssetUpdate.cs b/src/BuiltInTools/HotReloadClient/HotReloadStaticAssetUpdate.cs index 955e0cf07b78..51c33bae26cb 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadStaticAssetUpdate.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadStaticAssetUpdate.cs @@ -4,6 +4,10 @@ #nullable enable using System.Collections.Immutable; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.DotNet.HotReload; @@ -13,4 +17,18 @@ internal readonly struct HotReloadStaticAssetUpdate(string assemblyName, string public string AssemblyName { get; } = assemblyName; public ImmutableArray Content { get; } = content; public bool IsApplicationProject { get; } = isApplicationProject; + + public static async ValueTask CreateAsync(StaticWebAsset asset, CancellationToken cancellationToken) + { +#if NET + var blob = await File.ReadAllBytesAsync(asset.FilePath, cancellationToken); +#else + var blob = File.ReadAllBytes(asset.FilePath); +#endif + return new HotReloadStaticAssetUpdate( + assemblyName: asset.AssemblyName, + relativePath: asset.RelativeUrl, + content: ImmutableCollectionsMarshal.AsImmutableArray(blob), + asset.IsApplicationProject); + } } diff --git a/src/BuiltInTools/HotReloadClient/Utilities/PathExtensions.cs b/src/BuiltInTools/HotReloadClient/Utilities/PathExtensions.cs new file mode 100644 index 000000000000..c84aa5c628b5 --- /dev/null +++ b/src/BuiltInTools/HotReloadClient/Utilities/PathExtensions.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.IO; + +internal static partial class PathExtensions +{ +#if NET // binary compatibility + public static bool IsPathFullyQualified(string path) + => Path.IsPathFullyQualified(path); + + public static string Join(string? path1, string? path2) + => Path.Join(path1, path2); +#else + extension(Path) + { + public static bool IsPathFullyQualified(string path) + => Path.DirectorySeparatorChar == '\\' + ? !IsPartiallyQualified(path.AsSpan()) + : Path.IsPathRooted(path); + } + + // Copied from https://github.com/dotnet/runtime/blob/a6c5ba30aab998555e36aec7c04311935e1797ab/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L250 + + /// + /// Returns true if the path specified is relative to the current drive or working directory. + /// Returns false if the path is fixed to a specific drive or UNC path. This method does no + /// validation of the path (URIs will be returned as relative as a result). + /// + /// + /// Handles paths that use the alternate directory separator. It is a frequent mistake to + /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. + /// "C:a" is drive relative- meaning that it will be resolved against the current directory + /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory + /// will not be used to modify the path). + /// + private static bool IsPartiallyQualified(ReadOnlySpan path) + { + if (path.Length < 2) + { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return true; + } + + if (IsDirectorySeparator(path[0])) + { + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return !(path[1] == '?' || IsDirectorySeparator(path[1])); + } + + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return !((path.Length >= 3) + && (path[1] == Path.VolumeSeparatorChar) + && IsDirectorySeparator(path[2]) + // To match old behavior we'll check the drive character for validity as the path is technically + // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. + && IsValidDriveChar(path[0])); + } + + /// + /// True if the given character is a directory separator. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + + /// + /// Returns true if the given character is a valid drive letter + /// + internal static bool IsValidDriveChar(char value) + { + return (uint)((value | 0x20) - 'a') <= (uint)('z' - 'a'); + } + + // Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs + + private static readonly string s_directorySeparatorCharAsString = Path.DirectorySeparatorChar.ToString(); + + extension(Path) + { + public static string Join(string? path1, string? path2) + { + if (string.IsNullOrEmpty(path1)) + return path2 ?? string.Empty; + + if (string.IsNullOrEmpty(path2)) + return path1; + + return JoinInternal(path1, path2); + } + } + + private static string JoinInternal(string first, string second) + { + Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths"); + + bool hasSeparator = IsDirectorySeparator(first[^1]) || IsDirectorySeparator(second[0]); + + return hasSeparator ? + string.Concat(first, second) : + string.Concat(first, s_directorySeparatorCharAsString, second); + } +#endif +} diff --git a/src/BuiltInTools/HotReloadClient/Web/StaticWebAsset.cs b/src/BuiltInTools/HotReloadClient/Web/StaticWebAsset.cs new file mode 100644 index 000000000000..550f1b62555a --- /dev/null +++ b/src/BuiltInTools/HotReloadClient/Web/StaticWebAsset.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.HotReload; + +internal readonly struct StaticWebAsset(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject) +{ + public string FilePath => filePath; + public string RelativeUrl => relativeUrl; + public string AssemblyName => assemblyName; + public bool IsApplicationProject => isApplicationProject; + + public const string WebRoot = "wwwroot"; + public const string ManifestFileName = "staticwebassets.development.json"; + + public static bool IsScopedCssFile(string filePath) + => filePath.EndsWith(".razor.css", StringComparison.Ordinal) || + filePath.EndsWith(".cshtml.css", StringComparison.Ordinal); + + public static string GetScopedCssRelativeUrl(string applicationProjectFilePath, string containingProjectFilePath) + => WebRoot + "/" + GetScopedCssBundleFileName(applicationProjectFilePath, containingProjectFilePath); + + public static string GetScopedCssBundleFileName(string applicationProjectFilePath, string containingProjectFilePath) + { + var sourceProjectName = Path.GetFileNameWithoutExtension(containingProjectFilePath); + + return string.Equals(containingProjectFilePath, applicationProjectFilePath, StringComparison.OrdinalIgnoreCase) + ? $"{sourceProjectName}.styles.css" + : $"{sourceProjectName}.bundle.scp.css"; + } + + public static bool IsScopedCssBundleFile(string filePath) + => filePath.EndsWith(".bundle.scp.css", StringComparison.Ordinal) || + filePath.EndsWith(".styles.css", StringComparison.Ordinal); + + public static bool IsCompressedAssetFile(string filePath) + => filePath.EndsWith(".gz", StringComparison.Ordinal); + + public static string? GetRelativeUrl(string applicationProjectFilePath, string containingProjectFilePath, string assetFilePath) + => IsScopedCssFile(assetFilePath) + ? GetScopedCssRelativeUrl(applicationProjectFilePath, containingProjectFilePath) + : GetAppRelativeUrlFomDiskPath(containingProjectFilePath, assetFilePath); + + /// + /// For non scoped css, the only static files which apply are the ones under the wwwroot folder in that project. The relative path + /// will always start with wwwroot. eg: "wwwroot/css/styles.css" + /// + public static string? GetAppRelativeUrlFomDiskPath(string containingProjectFilePath, string assetFilePath) + { + var webRoot = "wwwroot" + Path.DirectorySeparatorChar; + var webRootDir = Path.Combine(Path.GetDirectoryName(containingProjectFilePath)!, webRoot); + + return assetFilePath.StartsWith(webRootDir, StringComparison.OrdinalIgnoreCase) + ? assetFilePath.Substring(webRootDir.Length - webRoot.Length).Replace("\\", "/") + : null; + } +} diff --git a/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetPattern.cs b/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetPattern.cs new file mode 100644 index 000000000000..6e2bcf742265 --- /dev/null +++ b/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetPattern.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. + +#nullable enable + +namespace Microsoft.DotNet.HotReload; + +internal sealed partial class StaticWebAssetPattern(string directory, string pattern, string baseUrl) +{ + public string Directory { get; } = directory; + public string Pattern { get; } = pattern; + public string BaseUrl { get; } = baseUrl; +} diff --git a/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetsManifest.cs b/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetsManifest.cs new file mode 100644 index 000000000000..22a1c4d5e0a5 --- /dev/null +++ b/src/BuiltInTools/HotReloadClient/Web/StaticWebAssetsManifest.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.HotReload; + +internal sealed class StaticWebAssetsManifest(ImmutableDictionary urlToPathMap, ImmutableArray discoveryPatterns) +{ + private static readonly JsonSerializerOptions s_options = new() + { + RespectNullableAnnotations = true, + }; + + private sealed class ManifestJson + { + public required List ContentRoots { get; init; } + public required ChildAssetJson Root { get; init; } + } + + private sealed class ChildAssetJson + { + public Dictionary? Children { get; init; } + public AssetInfoJson? Asset { get; init; } + public List? Patterns { get; init; } + } + + private sealed class AssetInfoJson + { + public required int ContentRootIndex { get; init; } + public required string SubPath { get; init; } + } + + private sealed class PatternJson + { + public required int ContentRootIndex { get; init; } + public required string Pattern { get; init; } + } + + /// + /// Maps relative URLs to file system paths. + /// + public ImmutableDictionary UrlToPathMap { get; } = urlToPathMap; + + /// + /// List of directory and search pattern pairs for discovering static web assets. + /// + public ImmutableArray DiscoveryPatterns { get; } = discoveryPatterns; + + public bool TryGetBundleFilePath(string bundleFileName, [NotNullWhen(true)] out string? filePath) + { + if (UrlToPathMap.TryGetValue(bundleFileName, out var bundleFilePath)) + { + filePath = bundleFilePath; + return true; + } + + foreach (var entry in UrlToPathMap) + { + var url = entry.Key; + var path = entry.Value; + + if (Path.GetFileName(path).Equals(bundleFileName, StringComparison.Ordinal)) + { + filePath = path; + return true; + } + } + + filePath = null; + return false; + } + + public static StaticWebAssetsManifest? TryParseFile(string path, ILogger logger) + { + Stream? stream; + + logger.LogDebug("Reading static web assets manifest file: '{FilePath}'.", path); + + try + { + stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + } + catch (Exception e) + { + logger.LogError("Failed to read '{FilePath}': {Message}", path, e.Message); + return null; + } + + try + { + return TryParse(stream, path, logger); + } + finally + { + stream.Dispose(); + } + } + + /// The format is invalid. + public static StaticWebAssetsManifest? TryParse(Stream stream, string filePath, ILogger logger) + { + ManifestJson? manifest; + + try + { + manifest = JsonSerializer.Deserialize(stream, s_options); + } + catch (JsonException e) + { + logger.LogError("Failed to parse '{FilePath}': {Message}", filePath, e.Message); + return null; + } + + if (manifest == null) + { + logger.LogError("Failed to parse '{FilePath}'", filePath); + return null; + } + + var validContentRoots = new string?[manifest.ContentRoots.Count]; + + for (var i = 0; i < validContentRoots.Length; i++) + { + var root = manifest.ContentRoots[i]; + if (Path.IsPathFullyQualified(root)) + { + validContentRoots[i] = root; + } + else + { + logger.LogWarning("Failed to parse '{FilePath}': ContentRoots path not fully qualified: {Root}", filePath, root); + } + } + + var urlToPathMap = ImmutableDictionary.CreateBuilder(); + var discoveryPatterns = ImmutableArray.CreateBuilder(); + + ProcessNode(manifest.Root, url: ""); + + return new StaticWebAssetsManifest(urlToPathMap.ToImmutable(), discoveryPatterns.ToImmutable()); + + void ProcessNode(ChildAssetJson node, string url) + { + if (node.Children != null) + { + foreach (var entry in node.Children) + { + var childName = entry.Key; + var child = entry.Value; + + ProcessNode(child, url: (url is []) ? childName : url + "/" + childName); + } + } + + if (node.Asset != null) + { + if (url == "") + { + logger.LogWarning("Failed to parse '{FilePath}': Asset has no URL", filePath); + return; + } + + if (!TryGetContentRoot(node.Asset.ContentRootIndex, out var root)) + { + return; + } + + urlToPathMap[url] = Path.Join(root, node.Asset.SubPath.Replace('/', Path.DirectorySeparatorChar)); + } + else if (node.Children == null) + { + logger.LogWarning("Failed to parse '{FilePath}': Missing Asset", filePath); + } + + if (node.Patterns != null) + { + foreach (var pattern in node.Patterns) + { + if (TryGetContentRoot(pattern.ContentRootIndex, out var root)) + { + discoveryPatterns.Add(new StaticWebAssetPattern(root, pattern.Pattern, url)); + } + } + } + + bool TryGetContentRoot(int index, [NotNullWhen(true)] out string? contentRoot) + { + if (index < 0 || index >= validContentRoots.Length) + { + logger.LogWarning("Failed to parse '{FilePath}': Invalid value of ContentRootIndex: {Value}", filePath, index); + contentRoot = null; + return false; + } + + contentRoot = validContentRoots[index]; + return contentRoot != null; + } + } + } +} diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs index 6a796e464d79..0bae06178441 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs @@ -40,6 +40,9 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti var muxerPath = Path.GetFullPath(Path.Combine(options.SdkDirectory, "..", "..", "dotnet" + PathUtilities.ExecutableExtension)); + // msbuild tasks depend on host path variable: + Environment.SetEnvironmentVariable(EnvironmentVariables.Names.DotnetHostPath, muxerPath); + var console = new PhysicalConsole(TestFlags.None); var reporter = new ConsoleReporter(console, suppressEmojis: false); var environmentOptions = EnvironmentOptions.FromEnvironment(muxerPath); diff --git a/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs index de999b11e0a5..74ce7a91dde2 100644 --- a/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs +++ b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs @@ -39,7 +39,7 @@ public void InstallBrowserLaunchTrigger( WebServerProcessStateObserver.Observe(projectNode, processSpec, url => { if (projectOptions.IsRootProject && - ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, key) => set.Add(key), projectNode.GetProjectInstanceId())) + ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, key) => set.Add(key), projectNode.ProjectInstance.GetId())) { // first build iteration of a root project: var launchUrl = GetLaunchUrl(launchProfile.LaunchUrl, url); diff --git a/src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs b/src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs index ea99bd9a22d4..5c6e2e9f4bef 100644 --- a/src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs +++ b/src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs @@ -45,7 +45,7 @@ public void Dispose() BrowserRefreshServer? server; bool hasExistingServer; - var key = projectNode.GetProjectInstanceId(); + var key = projectNode.ProjectInstance.GetId(); lock (_serversGuard) { @@ -74,7 +74,7 @@ public void Dispose() public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true)] out BrowserRefreshServer? server) { - var key = projectNode.GetProjectInstanceId(); + var key = projectNode.ProjectInstance.GetId(); lock (_serversGuard) { diff --git a/src/BuiltInTools/Watch/Build/BuildNames.cs b/src/BuiltInTools/Watch/Build/BuildNames.cs index f5f8c3463a3f..0b2b8d0ccd00 100644 --- a/src/BuiltInTools/Watch/Build/BuildNames.cs +++ b/src/BuiltInTools/Watch/Build/BuildNames.cs @@ -18,8 +18,6 @@ internal static class PropertyNames public const string HotReloadAutoRestart = nameof(HotReloadAutoRestart); public const string DefaultItemExcludes = nameof(DefaultItemExcludes); public const string CustomCollectWatchItems = nameof(CustomCollectWatchItems); - public const string UsingMicrosoftNETSdkRazor = nameof(UsingMicrosoftNETSdkRazor); - public const string DotNetWatchContentFiles = nameof(DotNetWatchContentFiles); public const string DotNetWatchBuild = nameof(DotNetWatchBuild); public const string DesignTimeBuild = nameof(DesignTimeBuild); public const string SkipCompilerExecution = nameof(SkipCompilerExecution); @@ -33,18 +31,24 @@ internal static class ItemNames public const string Compile = nameof(Compile); public const string Content = nameof(Content); public const string ProjectCapability = nameof(ProjectCapability); + public const string ScopedCssInput = nameof(ScopedCssInput); + public const string StaticWebAssetEndpoint = nameof(StaticWebAssetEndpoint); } internal static class MetadataNames { - public const string Watch = nameof(Watch); public const string TargetPath = nameof(TargetPath); + public const string AssetFile = nameof(AssetFile); + public const string EndpointProperties = nameof(EndpointProperties); } internal static class TargetNames { public const string Compile = nameof(Compile); + public const string CompileDesignTime = nameof(CompileDesignTime); public const string Restore = nameof(Restore); + public const string ResolveScopedCssInputs = nameof(ResolveScopedCssInputs); + public const string ResolveReferencedProjectsStaticWebAssets = nameof(ResolveReferencedProjectsStaticWebAssets); public const string GenerateComputedBuildStaticWebAssets = nameof(GenerateComputedBuildStaticWebAssets); public const string ReferenceCopyLocalPathsOutputGroup = nameof(ReferenceCopyLocalPathsOutputGroup); } diff --git a/src/BuiltInTools/Watch/Build/BuildReporter.cs b/src/BuiltInTools/Watch/Build/BuildReporter.cs index f062e112d457..6410476dc3c8 100644 --- a/src/BuiltInTools/Watch/Build/BuildReporter.cs +++ b/src/BuiltInTools/Watch/Build/BuildReporter.cs @@ -23,12 +23,12 @@ public void ReportWatchedFiles(Dictionary fileItems) { logger.Log(MessageDescriptor.WatchingFilesForChanges, fileItems.Count); - if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest)) + if (logger.IsEnabled(LogLevel.Trace)) { foreach (var file in fileItems.Values) { - logger.Log(MessageDescriptor.WatchingFilesForChanges_FilePath, file.StaticWebAssetPath != null - ? $"{file.FilePath}{Path.PathSeparator}{file.StaticWebAssetPath}" + logger.Log(MessageDescriptor.WatchingFilesForChanges_FilePath, file.StaticWebAssetRelativeUrl != null + ? $"{file.FilePath}{Path.PathSeparator}{string.Join(Path.PathSeparator, file.StaticWebAssetRelativeUrl)}" : $"{file.FilePath}"); } } diff --git a/src/BuiltInTools/Watch/Build/EvaluationResult.cs b/src/BuiltInTools/Watch/Build/EvaluationResult.cs index bb04a3683a7e..829f527774e0 100644 --- a/src/BuiltInTools/Watch/Build/EvaluationResult.cs +++ b/src/BuiltInTools/Watch/Build/EvaluationResult.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using Microsoft.Build.Execution; using Microsoft.Build.Graph; +using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; -internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph projectGraph) +internal sealed class EvaluationResult(ProjectGraph projectGraph, IReadOnlyDictionary files, IReadOnlyDictionary staticWebAssetsManifests) { public readonly IReadOnlyDictionary Files = files; public readonly ProjectGraph ProjectGraph = projectGraph; @@ -26,9 +28,17 @@ private static IReadOnlySet CreateBuildFileSet(ProjectGraph projectGraph public IReadOnlySet BuildFiles => _lazyBuildFiles.Value; + public IReadOnlyDictionary StaticWebAssetsManifests + => staticWebAssetsManifests; + public void WatchFiles(FileWatcher fileWatcher) { fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true); + + fileWatcher.WatchContainingDirectories( + StaticWebAssetsManifests.Values.SelectMany(static manifest => manifest.DiscoveryPatterns.Select(static pattern => pattern.Directory)), + includeSubdirectories: true); + fileWatcher.WatchFiles(BuildFiles); } @@ -41,9 +51,7 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera .SetItem(PropertyNames.DotNetWatchBuild, "true") .SetItem(PropertyNames.DesignTimeBuild, "true") .SetItem(PropertyNames.SkipCompilerExecution, "true") - .SetItem(PropertyNames.ProvideCommandLineArgs, "true") - // F# targets depend on host path variable: - .SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath); + .SetItem(PropertyNames.ProvideCommandLineArgs, "true"); } /// @@ -87,6 +95,7 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera } var fileItems = new Dictionary(); + var staticWebAssetManifests = new Dictionary(); foreach (var project in projectGraph.ProjectNodesTopologicallySorted) { @@ -101,11 +110,15 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera continue; } - var customCollectWatchItems = projectInstance.GetStringListPropertyValue(PropertyNames.CustomCollectWatchItems); + var targets = GetBuildTargets(projectInstance, environmentOptions); + if (targets is []) + { + continue; + } using (var loggers = buildReporter.GetLoggers(projectInstance.FullPath, "DesignTimeBuild")) { - if (!projectInstance.Build([TargetNames.Compile, .. customCollectWatchItems], loggers)) + if (!projectInstance.Build(targets, loggers)) { logger.LogError("Failed to build project '{Path}'.", projectInstance.FullPath); loggers.ReportOutput(); @@ -116,36 +129,46 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera var projectPath = projectInstance.FullPath; var projectDirectory = Path.GetDirectoryName(projectPath)!; - // TODO: Compile and AdditionalItems should be provided by Roslyn - var items = projectInstance.GetItems(ItemNames.Compile) - .Concat(projectInstance.GetItems(ItemNames.AdditionalFiles)) - .Concat(projectInstance.GetItems(ItemNames.Watch)); - - foreach (var item in items) + if (targets.Contains(TargetNames.GenerateComputedBuildStaticWebAssets) && + projectInstance.GetIntermediateOutputDirectory() is { } outputDir && + StaticWebAssetsManifest.TryParseFile(Path.Combine(outputDir, StaticWebAsset.ManifestFileName), logger) is { } manifest) { - AddFile(item.EvaluatedInclude, staticWebAssetPath: null); - } + staticWebAssetManifests.Add(projectInstance.GetId(), manifest); - if (!environmentOptions.SuppressHandlingStaticContentFiles && - projectInstance.GetBooleanPropertyValue(PropertyNames.UsingMicrosoftNETSdkRazor) && - projectInstance.GetBooleanPropertyValue(PropertyNames.DotNetWatchContentFiles, defaultValue: true)) - { - foreach (var item in projectInstance.GetItems(ItemNames.Content)) + // watch asset files, but not bundle files as they are regenarated when scoped CSS files are updated: + foreach (var (relativeUrl, filePath) in manifest.UrlToPathMap) { - if (item.GetBooleanMetadataValue(MetadataNames.Watch, defaultValue: true)) + if (!StaticWebAsset.IsCompressedAssetFile(filePath) && !StaticWebAsset.IsScopedCssBundleFile(filePath)) { - var relativeUrl = item.EvaluatedInclude.Replace('\\', '/'); - if (relativeUrl.StartsWith("wwwroot/")) - { - AddFile(item.EvaluatedInclude, staticWebAssetPath: relativeUrl); - } + AddFile(filePath, staticWebAssetRelativeUrl: relativeUrl); } } } - void AddFile(string include, string? staticWebAssetPath) + // Adds file items for scoped css files. + // Scoped css files are bundled into a single entry per project that is represented in the static web assets manifest, + // but we need to watch the original individual files. + if (targets.Contains(TargetNames.ResolveScopedCssInputs)) { - var filePath = Path.GetFullPath(Path.Combine(projectDirectory, include)); + foreach (var item in projectInstance.GetItems(ItemNames.ScopedCssInput)) + { + AddFile(item.EvaluatedInclude, staticWebAssetRelativeUrl: null); + } + } + + // Add Watch items after other items so that we don't override properties set above. + var items = projectInstance.GetItems(ItemNames.Compile) + .Concat(projectInstance.GetItems(ItemNames.AdditionalFiles)) + .Concat(projectInstance.GetItems(ItemNames.Watch)); + + foreach (var item in items) + { + AddFile(item.EvaluatedInclude, staticWebAssetRelativeUrl: null); + } + + void AddFile(string relativePath, string? staticWebAssetRelativeUrl) + { + var filePath = Path.GetFullPath(Path.Combine(projectDirectory, relativePath)); if (!fileItems.TryGetValue(filePath, out var existingFile)) { @@ -153,7 +176,7 @@ void AddFile(string include, string? staticWebAssetPath) { FilePath = filePath, ContainingProjectPaths = [projectPath], - StaticWebAssetPath = staticWebAssetPath, + StaticWebAssetRelativeUrl = staticWebAssetRelativeUrl, }); } else if (!existingFile.ContainingProjectPaths.Contains(projectPath)) @@ -166,6 +189,43 @@ void AddFile(string include, string? staticWebAssetPath) buildReporter.ReportWatchedFiles(fileItems); - return new EvaluationResult(fileItems, projectGraph); + return new EvaluationResult(projectGraph, fileItems, staticWebAssetManifests); + } + + private static string[] GetBuildTargets(ProjectInstance projectInstance, EnvironmentOptions environmentOptions) + { + var compileTarget = projectInstance.Targets.ContainsKey(TargetNames.CompileDesignTime) + ? TargetNames.CompileDesignTime + : projectInstance.Targets.ContainsKey(TargetNames.Compile) + ? TargetNames.Compile + : null; + + if (compileTarget == null) + { + return []; + } + + var targets = new List + { + compileTarget + }; + + if (!environmentOptions.SuppressHandlingStaticWebAssets) + { + // generates static file asset manifest + if (projectInstance.Targets.ContainsKey(TargetNames.GenerateComputedBuildStaticWebAssets)) + { + targets.Add(TargetNames.GenerateComputedBuildStaticWebAssets); + } + + // populates ScopedCssInput items: + if (projectInstance.Targets.ContainsKey(TargetNames.ResolveScopedCssInputs)) + { + targets.Add(TargetNames.ResolveScopedCssInputs); + } + } + + targets.AddRange(projectInstance.GetStringListPropertyValue(PropertyNames.CustomCollectWatchItems)); + return [.. targets]; } } diff --git a/src/BuiltInTools/Watch/Build/FileItem.cs b/src/BuiltInTools/Watch/Build/FileItem.cs index acf044055fae..bd6719f2ca00 100644 --- a/src/BuiltInTools/Watch/Build/FileItem.cs +++ b/src/BuiltInTools/Watch/Build/FileItem.cs @@ -14,8 +14,6 @@ internal readonly record struct FileItem /// public required List ContainingProjectPaths { get; init; } - public string? StaticWebAssetPath { get; init; } - - public bool IsStaticFile => StaticWebAssetPath != null; + public string? StaticWebAssetRelativeUrl { get; init; } } } diff --git a/src/BuiltInTools/Watch/Build/FilePathExclusions.cs b/src/BuiltInTools/Watch/Build/FilePathExclusions.cs index 305ef3c0ab9e..b4f921d875f6 100644 --- a/src/BuiltInTools/Watch/Build/FilePathExclusions.cs +++ b/src/BuiltInTools/Watch/Build/FilePathExclusions.cs @@ -40,7 +40,7 @@ public static FilePathExclusions Create(ProjectGraph projectGraph) // If default items are not enabled exclude just the output directories. TryAddOutputDir(projectNode.GetOutputDirectory()); - TryAddOutputDir(projectNode.GetIntermediateOutputDirectory()); + TryAddOutputDir(projectNode.ProjectInstance.GetIntermediateOutputDirectory()); void TryAddOutputDir(string? dir) { diff --git a/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs index 40314297b76a..111dff988e47 100644 --- a/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs +++ b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs @@ -10,13 +10,16 @@ namespace Microsoft.DotNet.Watch; internal static class ProjectGraphUtilities { public static string GetDisplayName(this ProjectGraphNode projectNode) - => $"{Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath)} ({projectNode.GetTargetFramework()})"; + => projectNode.ProjectInstance.GetDisplayName(); - public static string GetTargetFramework(this ProjectGraphNode projectNode) - => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFramework); + public static string GetDisplayName(this ProjectInstance project) + => $"{Path.GetFileNameWithoutExtension(project.FullPath)} ({project.GetTargetFramework()})"; - public static IEnumerable GetTargetFrameworks(this ProjectGraphNode projectNode) - => projectNode.GetStringListPropertyValue(PropertyNames.TargetFrameworks); + public static string GetTargetFramework(this ProjectInstance project) + => project.GetPropertyValue(PropertyNames.TargetFramework); + + public static IEnumerable GetTargetFrameworks(this ProjectInstance project) + => project.GetStringListPropertyValue(PropertyNames.TargetFrameworks); public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode) { @@ -54,8 +57,8 @@ public static bool IsWebApp(this ProjectGraphNode projectNode) public static string GetAssemblyName(this ProjectGraphNode projectNode) => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetName); - public static string? GetIntermediateOutputDirectory(this ProjectGraphNode projectNode) - => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.IntermediateOutputPath) is { Length: >0 } path ? Path.Combine(projectNode.ProjectInstance.Directory, path) : null; + public static string? GetIntermediateOutputDirectory(this ProjectInstance project) + => project.GetPropertyValue(PropertyNames.IntermediateOutputPath) is { Length: >0 } path ? Path.Combine(project.Directory, path) : null; public static IEnumerable GetCapabilities(this ProjectGraphNode projectNode) => projectNode.ProjectInstance.GetItems(ItemNames.ProjectCapability).Select(item => item.EvaluatedInclude); @@ -120,6 +123,6 @@ private static IEnumerable GetTransitiveProjects(IEnumerable

new(projectNode.ProjectInstance.FullPath, projectNode.GetTargetFramework()); + public static ProjectInstanceId GetId(this ProjectInstance project) + => new(project.FullPath, project.GetTargetFramework()); } diff --git a/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs b/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs new file mode 100644 index 000000000000..35931038b8ec --- /dev/null +++ b/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs @@ -0,0 +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.Build.Globbing; + +namespace Microsoft.DotNet.HotReload; + +internal sealed partial class StaticWebAssetPattern +{ + public MSBuildGlob Glob => field ??= MSBuildGlob.Parse(Directory, Pattern); +} diff --git a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs index 8eb705b17926..0569229e3233 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs @@ -30,7 +30,7 @@ internal sealed record EnvironmentOptions( string MuxerPath, TimeSpan? ProcessCleanupTimeout, bool IsPollingEnabled = false, - bool SuppressHandlingStaticContentFiles = false, + bool SuppressHandlingStaticWebAssets = false, bool SuppressMSBuildIncrementalism = false, bool SuppressLaunchBrowser = false, bool SuppressBrowserRefresh = false, @@ -49,7 +49,7 @@ internal sealed record EnvironmentOptions( MuxerPath: ValidateMuxerPath(muxerPath), ProcessCleanupTimeout: EnvironmentVariables.ProcessCleanupTimeout, IsPollingEnabled: EnvironmentVariables.IsPollingEnabled, - SuppressHandlingStaticContentFiles: EnvironmentVariables.SuppressHandlingStaticContentFiles, + SuppressHandlingStaticWebAssets: EnvironmentVariables.SuppressHandlingStaticWebAssets, SuppressMSBuildIncrementalism: EnvironmentVariables.SuppressMSBuildIncrementalism, SuppressLaunchBrowser: EnvironmentVariables.SuppressLaunchBrowser, SuppressBrowserRefresh: EnvironmentVariables.SuppressBrowserRefresh, diff --git a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs index 41ac7d88edea..6a89a52cdf88 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs @@ -13,6 +13,7 @@ public static class Names public const string DotnetWatchIteration = "DOTNET_WATCH_ITERATION"; public const string DotnetLaunchProfile = "DOTNET_LAUNCH_PROFILE"; + public const string DotnetHostPath = "DOTNET_HOST_PATH"; public const string DotNetWatchHotReloadNamedPipeName = HotReload.AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName; public const string DotNetStartupHooks = HotReload.AgentEnvironmentVariables.DotNetStartupHooks; @@ -47,7 +48,7 @@ public static LogLevel? CliLogLevel ""; #endif - public static bool SuppressHandlingStaticContentFiles => ReadBool("DOTNET_WATCH_SUPPRESS_STATIC_FILE_HANDLING"); + public static bool SuppressHandlingStaticWebAssets => ReadBool("DOTNET_WATCH_SUPPRESS_STATIC_FILE_HANDLING"); public static bool SuppressMSBuildIncrementalism => ReadBool("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM"); public static bool SuppressLaunchBrowser => ReadBool("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER"); public static bool SuppressBrowserRefresh => ReadBool(Names.SuppressBrowserRefresh); diff --git a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs index 43d60c17aefe..3f5bff195e92 100644 --- a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs +++ b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs @@ -44,7 +44,7 @@ protected virtual DirectoryWatcher CreateDirectoryWatcher(string directory, Immu var watcher = DirectoryWatcher.Create(directory, fileNames, environmentOptions.IsPollingEnabled, includeSubdirectories); if (watcher is EventBasedDirectoryWatcher eventBasedWatcher) { - eventBasedWatcher.Logger = message => logger.LogDebug(message); + eventBasedWatcher.Logger = message => logger.LogTrace(message); } return watcher; diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs index e95ccc1be34f..a8f73dbb48df 100644 --- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using Microsoft.Build.Execution; using Microsoft.Build.Graph; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -15,9 +16,8 @@ namespace Microsoft.DotNet.Watch internal sealed class CompilationHandler : IDisposable { public readonly IncrementalMSBuildWorkspace Workspace; - private readonly ILogger _logger; + private readonly DotNetWatchContext _context; private readonly HotReloadService _hotReloadService; - private readonly ProcessRunner _processRunner; ///

/// Lock to synchronize: @@ -39,11 +39,10 @@ internal sealed class CompilationHandler : IDisposable private bool _isDisposed; - public CompilationHandler(ILogger logger, ProcessRunner processRunner) + public CompilationHandler(DotNetWatchContext context) { - _logger = logger; - _processRunner = processRunner; - Workspace = new IncrementalMSBuildWorkspace(logger); + _context = context; + Workspace = new IncrementalMSBuildWorkspace(context.Logger); _hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities())); } @@ -53,9 +52,12 @@ public void Dispose() Workspace?.Dispose(); } + private ILogger Logger + => _context.Logger; + public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken) { - _logger.LogDebug("Terminating remaining child processes."); + Logger.LogDebug("Terminating remaining child processes."); await TerminateNonRootProcessesAsync(projectPaths: null, cancellationToken); Dispose(); } @@ -75,7 +77,7 @@ private void DiscardPreviousUpdates(ImmutableArray projectsToBeRebuil } public async ValueTask StartSessionAsync(CancellationToken cancellationToken) { - _logger.Log(MessageDescriptor.HotReloadSessionStarting); + Logger.Log(MessageDescriptor.HotReloadSessionStarting); var solution = Workspace.CurrentSolution; @@ -95,7 +97,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) } } - _logger.Log(MessageDescriptor.HotReloadSessionStarted); + Logger.Log(MessageDescriptor.HotReloadSessionStarted); } public async Task TrackRunningProjectAsync( @@ -140,7 +142,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) }; var launchResult = new ProcessLaunchResult(); - var runningProcess = _processRunner.RunAsync(processSpec, clients.ClientLogger, launchResult, processTerminationSource.Token); + var runningProcess = _context.ProcessRunner.RunAsync(processSpec, clients.ClientLogger, launchResult, processTerminationSource.Token); if (launchResult.ProcessId == null) { // error already reported @@ -232,7 +234,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) catch (OperationCanceledException) when (processExitedSource.IsCancellationRequested) { // Process exited during initialization. This should not happen since we control the process during this time. - _logger.LogError("Failed to launch '{ProjectPath}'. Process {PID} exited during initialization.", projectPath, launchResult.ProcessId); + Logger.LogError("Failed to launch '{ProjectPath}'. Process {PID} exited during initialization.", projectPath, launchResult.ProcessId); return null; } } @@ -281,7 +283,7 @@ private ImmutableArray GetAggregateCapabilities() .Order() .ToImmutableArray(); - _logger.Log(MessageDescriptor.HotReloadCapabilities, string.Join(" ", capabilities)); + Logger.Log(MessageDescriptor.HotReloadCapabilities, string.Join(" ", capabilities)); return capabilities; } @@ -341,7 +343,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C { _hotReloadService.DiscardUpdate(); - _logger.Log(MessageDescriptor.HotReloadSuspended); + Logger.Log(MessageDescriptor.HotReloadSuspended); await Task.Delay(-1, cancellationToken); return ([], [], [], []); @@ -409,7 +411,7 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT return projectsWithPath[0]; } - return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase)); + return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.ProjectInstance.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase)); } private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, ImmutableDictionary runningProjectInfos, CancellationToken cancellationToken) @@ -420,11 +422,11 @@ private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, Im break; case HotReloadService.Status.NoChangesToApply: - _logger.Log(MessageDescriptor.NoCSharpChangesToApply); + Logger.Log(MessageDescriptor.NoCSharpChangesToApply); break; case HotReloadService.Status.Blocked: - _logger.Log(MessageDescriptor.UnableToApplyChanges); + Logger.Log(MessageDescriptor.UnableToApplyChanges); break; default: @@ -433,7 +435,7 @@ private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, Im if (!updates.ProjectsToRestart.IsEmpty) { - _logger.Log(MessageDescriptor.RestartNeededToApplyChanges); + Logger.Log(MessageDescriptor.RestartNeededToApplyChanges); } var errorsToDisplayInApp = new List(); @@ -515,7 +517,7 @@ void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, strin var display = CSharpDiagnosticFormatter.Instance.Format(diagnostic); var args = new[] { autoPrefix, display }; - _logger.Log(descriptor, args); + Logger.Log(descriptor, args); if (autoPrefix != "") { @@ -551,19 +553,27 @@ static MessageDescriptor GetMessageDescriptor(Diagnostic diagnostic, bool verbos } } - public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList files, ProjectNodeMap projectMap, CancellationToken cancellationToken) - { - var allFilesHandled = true; + private static readonly string[] s_targets = [TargetNames.GenerateComputedBuildStaticWebAssets, TargetNames.ResolveReferencedProjectsStaticWebAssets]; - var updates = new Dictionary>(); + private static bool HasScopedCssTargets(ProjectInstance projectInstance) + => s_targets.All(t => projectInstance.Targets.ContainsKey(t)); + + public async ValueTask HandleStaticAssetChangesAsync( + IReadOnlyList files, + ProjectNodeMap projectMap, + IReadOnlyDictionary manifests, + CancellationToken cancellationToken) + { + var assets = new Dictionary>(); + var projectInstancesToRegenerate = new HashSet(); foreach (var changedFile in files) { var file = changedFile.Item; + var isScopedCss = StaticWebAsset.IsScopedCssFile(file.FilePath); - if (file.StaticWebAssetPath is null) + if (!isScopedCss && file.StaticWebAssetRelativeUrl is null) { - allFilesHandled = false; continue; } @@ -572,48 +582,145 @@ public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList? failedApplicationProjectInstances = null; + if (projectInstancesToRegenerate.Count > 0) + { + var buildReporter = new BuildReporter(_context.BuildLogger, _context.Options, _context.EnvironmentOptions); + + // Note: MSBuild only allows one build at a time in a process. + foreach (var projectInstance in projectInstancesToRegenerate) + { + Logger.LogDebug("[{Project}] Regenerating scoped CSS bundle.", projectInstance.GetDisplayName()); + + using var loggers = buildReporter.GetLoggers(projectInstance.FullPath, "ScopedCss"); + + // Deep copy so that we don't pollute the project graph: + if (!projectInstance.DeepCopy().Build(s_targets, loggers)) + { + loggers.ReportOutput(); + + failedApplicationProjectInstances ??= []; + failedApplicationProjectInstances.Add(projectInstance); + } + } } - var tasks = updates.Select(async entry => + var tasks = assets.Select(async entry => { - var (runningProject, assets) = entry; - using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); - await runningProject.Clients.ApplyStaticAssetUpdatesAsync(assets, processCommunicationCancellationSource.Token); + var (applicationProjectInstance, instanceAssets) = entry; + + if (failedApplicationProjectInstances?.Contains(applicationProjectInstance) == true) + { + return; + } + + if (!TryGetRunningProject(applicationProjectInstance.FullPath, out var runningProjects)) + { + return; + } + + foreach (var runningProject in runningProjects) + { + using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); + await runningProject.Clients.ApplyStaticAssetUpdatesAsync(instanceAssets.Values, processCommunicationCancellationSource.Token); + } }); await Task.WhenAll(tasks).WaitAsync(cancellationToken); - _logger.Log(MessageDescriptor.HotReloadOfStaticAssetsSucceeded); - - return allFilesHandled; + Logger.Log(MessageDescriptor.StaticAssetsReloaded); } /// diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs index 52df0fa62829..ab85d94c08f0 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -3,6 +3,8 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Text.Encodings.Web; +using Microsoft.Build.Execution; using Microsoft.Build.Graph; using Microsoft.CodeAnalysis; using Microsoft.DotNet.HotReload; @@ -113,8 +115,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) } var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, _context.Logger); - compilationHandler = new CompilationHandler(_context.Logger, _context.ProcessRunner); - var scopedCssFileHandler = new ScopedCssFileHandler(_context.Logger, _context.BuildLogger, projectMap, _context.BrowserRefreshServerFactory, _context.Options, _context.EnvironmentOptions); + compilationHandler = new CompilationHandler(_context); var projectLauncher = new ProjectLauncher(_context, projectMap, compilationHandler, iteration); evaluationResult.ItemExclusions.Report(_context.Logger); @@ -260,13 +261,9 @@ void FileChangedCallback(ChangedPath change) var stopwatch = Stopwatch.StartNew(); HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.StaticHandler); - await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, iterationCancellationToken); + await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, evaluationResult.StaticWebAssetsManifests, iterationCancellationToken); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.StaticHandler); - HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.ScopedCssHandler); - await scopedCssFileHandler.HandleFileChangesAsync(changedFiles, iterationCancellationToken); - HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.ScopedCssHandler); - HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler); var (managedCodeUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync( @@ -399,7 +396,7 @@ await Task.WhenAll( _context.Logger.Log(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds); - async Task> CaptureChangedFilesSnapshot(ImmutableArray rebuiltProjects) + async Task> CaptureChangedFilesSnapshot(ImmutableArray rebuiltProjects) { var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []); if (changedPaths is []) @@ -432,22 +429,14 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra new FileItem() { FilePath = changedPath.Path, ContainingProjectPaths = [] }, changedPath.Kind); }) - .ToImmutableList(); + .ToList(); ReportFileChanges(changedFiles); - // 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 also need to re-evaluate the project if any project files have been modified. - // We don't need to rebuild and restart the application though. - var fileAdded = changedFiles.Any(f => f.Kind is ChangeKind.Add); - var projectChanged = !fileAdded && changedFiles.Any(f => evaluationResult.BuildFiles.Contains(f.Item.FilePath)); - var evaluationRequired = fileAdded || projectChanged; + AnalyzeFileChanges(changedFiles, evaluationResult, out var evaluationRequired); if (evaluationRequired) { - _context.Logger.Log(fileAdded ? MessageDescriptor.FileAdditionTriggeredReEvaluation : MessageDescriptor.ProjectChangeTriggeredReEvaluation); - // TODO: consider re-evaluating only affected projects instead of the whole graph. evaluationResult = await EvaluateRootProjectAsync(restore: true, iterationCancellationToken); @@ -463,9 +452,14 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra } // Update files in the change set with new evaluation info. - changedFiles = [.. changedFiles - .Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f) - ]; + for (var i = 0; i < changedFiles.Count; i++) + { + var file = changedFiles[i]; + if (evaluationResult.Files.TryGetValue(file.Item.FilePath, out var evaluatedFile)) + { + changedFiles[i] = file with { Item = evaluatedFile }; + } + } _context.Logger.Log(MessageDescriptor.ReEvaluationCompleted); } @@ -478,13 +472,13 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra var rebuiltProjectPaths = rebuiltProjects.ToHashSet(); var newAccumulator = ImmutableList.Empty; - var newChangedFiles = ImmutableList.Empty; + var newChangedFiles = new List(); foreach (var file in changedFiles) { if (file.Item.ContainingProjectPaths.All(containingProjectPath => rebuiltProjectPaths.Contains(containingProjectPath))) { - newChangedFiles = newChangedFiles.Add(file); + newChangedFiles.Add(file); } else { @@ -504,7 +498,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken); } - return changedFiles; + return [.. changedFiles]; } } } @@ -560,6 +554,104 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra } } + private void AnalyzeFileChanges( + List changedFiles, + EvaluationResult evaluationResult, + out bool evaluationRequired) + { + // If any build file changed (project, props, targets) we need to re-evaluate the projects. + // Currently we re-evaluate the whole project graph even if only a single project file changed. + if (changedFiles.Select(f => f.Item.FilePath).FirstOrDefault(path => evaluationResult.BuildFiles.Contains(path) || MatchesBuildFile(path)) is { } firstBuildFilePath) + { + _context.Logger.Log(MessageDescriptor.ProjectChangeTriggeredReEvaluation, firstBuildFilePath); + evaluationRequired = true; + return; + } + + for (var i = 0; i < changedFiles.Count; i++) + { + var changedFile = changedFiles[i]; + var filePath = changedFile.Item.FilePath; + + if (changedFile.Kind is ChangeKind.Add) + { + if (MatchesStaticWebAssetFilePattern(evaluationResult, filePath, out var staticWebAssetUrl)) + { + changedFiles[i] = changedFile with + { + Item = changedFile.Item with { StaticWebAssetRelativeUrl = staticWebAssetUrl } + }; + } + else + { + // TODO: https://github.com/dotnet/sdk/issues/52390 + // Get patterns from evaluation that match Compile, AdditionalFile, AnalyzerConfigFile items. + // Avoid re-evaluating on addition of files that don't affect the project. + + // project file or other file: + _context.Logger.Log(MessageDescriptor.FileAdditionTriggeredReEvaluation, filePath); + evaluationRequired = true; + return; + } + } + } + + evaluationRequired = false; + } + + /// + /// True if the file path looks like a file that might be imported by MSBuild. + /// + private static bool MatchesBuildFile(string filePath) + { + var extension = Path.GetExtension(filePath); + return extension.Equals(".props", PathUtilities.OSSpecificPathComparison) + || extension.Equals(".targets", PathUtilities.OSSpecificPathComparison) + || extension.EndsWith("proj", PathUtilities.OSSpecificPathComparison) + || extension.Equals("projitems", PathUtilities.OSSpecificPathComparison) // shared project items + || string.Equals(Path.GetFileName(filePath), "global.json", PathUtilities.OSSpecificPathComparison); + } + + /// + /// Determines if the given file path is a static web asset file path based on + /// the discovery patterns. + /// + private static bool MatchesStaticWebAssetFilePattern(EvaluationResult evaluationResult, string filePath, out string? staticWebAssetUrl) + { + staticWebAssetUrl = null; + + if (StaticWebAsset.IsScopedCssFile(filePath)) + { + return true; + } + + foreach (var (_, manifest) in evaluationResult.StaticWebAssetsManifests) + { + foreach (var pattern in manifest.DiscoveryPatterns) + { + var match = pattern.Glob.MatchInfo(filePath); + if (match.IsMatch) + { + var dirUrl = match.WildcardDirectoryPartMatchGroup.Replace(Path.DirectorySeparatorChar, '/'); + + Debug.Assert(!dirUrl.EndsWith('/')); + Debug.Assert(!pattern.BaseUrl.EndsWith('/')); + + var url = UrlEncoder.Default.Encode(dirUrl + "/" + match.FilenamePartMatchGroup); + if (pattern.BaseUrl != "") + { + url = pattern.BaseUrl + "/" + url; + } + + staticWebAssetUrl = url; + return true; + } + } + } + + return false; + } + private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray projectPaths, CancellationToken cancellationToken) { var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer); diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs index 92af26786b5a..f14b000bb34c 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs @@ -14,7 +14,6 @@ public enum StartType Main, StaticHandler, CompilationHandler, - ScopedCssHandler } internal sealed class Keywords diff --git a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs deleted file mode 100644 index 09e33759e7a4..000000000000 --- a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - - -using Microsoft.Build.Graph; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DotNet.Watch -{ - internal sealed class ScopedCssFileHandler(ILogger logger, ILogger buildLogger, ProjectNodeMap projectMap, BrowserRefreshServerFactory browserConnector, GlobalOptions options, EnvironmentOptions environmentOptions) - { - private const string BuildTargetName = TargetNames.GenerateComputedBuildStaticWebAssets; - - 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].Item; - - if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) && - !file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal)) - { - continue; - } - - hasApplicableFiles = true; - logger.LogDebug("Handling file change event for scoped css file {FilePath}.", file.FilePath); - foreach (var containingProjectPath in file.ContainingProjectPaths) - { - if (!projectMap.Map.TryGetValue(containingProjectPath, out var projectNodes)) - { - // Shouldn't happen. - logger.LogWarning("Project '{Path}' not found in the project graph.", containingProjectPath); - continue; - } - - // Build and refresh each instance (TFM) of the project. - foreach (var projectNode in projectNodes) - { - // The outer build project instance (that specifies TargetFrameworks) won't have the target. - if (projectNode.ProjectInstance.Targets.ContainsKey(BuildTargetName)) - { - projectsToRefresh.Add(projectNode); - } - } - } - } - - if (!hasApplicableFiles) - { - return; - } - - var buildReporter = new BuildReporter(buildLogger, options, environmentOptions); - - var buildTasks = projectsToRefresh.Select(projectNode => Task.Run(() => - { - using var loggers = buildReporter.GetLoggers(projectNode.ProjectInstance.FullPath, BuildTargetName); - - // Deep copy so that we don't pollute the project graph: - if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, loggers)) - { - loggers.ReportOutput(); - return null; - } - - return projectNode; - })); - - var buildResults = await Task.WhenAll(buildTasks).WaitAsync(cancellationToken); - - var browserRefreshTasks = buildResults.Where(p => p != null)!.GetAncestorsAndSelf().Select(async projectNode => - { - if (browserConnector.TryGetRefreshServer(projectNode, out var browserRefreshServer)) - { - // We'd like an accurate scoped css path, but this needs a lot of work to wire-up now. - // We'll handle this as part of https://github.com/dotnet/aspnetcore/issues/31217. - // For now, we'll make it look like some css file which would cause JS to update a - // single file if it's from the current project, or all locally hosted css files if it's a file from - // referenced project. - var relativeUrl = Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath) + ".css"; - await browserRefreshServer.UpdateStaticAssetsAsync([relativeUrl], cancellationToken); - } - }); - - await Task.WhenAll(browserRefreshTasks).WaitAsync(cancellationToken); - - var successfulCount = buildResults.Sum(r => r != null ? 1 : 0); - - if (successfulCount == buildResults.Length) - { - logger.Log(MessageDescriptor.HotReloadOfScopedCssSucceeded); - } - else if (successfulCount > 0) - { - logger.Log(MessageDescriptor.HotReloadOfScopedCssPartiallySucceeded, successfulCount, buildResults.Length); - } - else - { - logger.Log(MessageDescriptor.HotReloadOfScopedCssFailed); - } - } - } -} diff --git a/src/BuiltInTools/Watch/UI/IReporter.cs b/src/BuiltInTools/Watch/UI/IReporter.cs index 82b60996e1ab..079ce3e46101 100644 --- a/src/BuiltInTools/Watch/UI/IReporter.cs +++ b/src/BuiltInTools/Watch/UI/IReporter.cs @@ -196,12 +196,12 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition) public static readonly MessageDescriptor ConnectedToRefreshServer = Create(LogEvents.ConnectedToRefreshServer, Emoji.Default); public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, LogLevel.Information); public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, LogLevel.Information); - public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Trace); + public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Trace); + public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Trace); + public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation: '{0}'.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation: '{0}'.", Emoji.Watch, LogLevel.Debug); public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, LogLevel.Information); public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, LogLevel.Information); public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, LogLevel.Error); @@ -215,10 +215,7 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition) public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, LogLevel.Debug); public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, LogLevel.Warning); public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, LogLevel.Information); - public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, LogLevel.Information); - public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, LogLevel.Error); - public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor StaticAssetsReloaded = Create("Static assets reloaded.", Emoji.HotReload, LogLevel.Information); public static readonly MessageDescriptor SendingStaticAssetUpdateRequest = Create(LogEvents.SendingStaticAssetUpdateRequest, Emoji.Default); public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, LogLevel.Debug); public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, LogLevel.Information); @@ -232,7 +229,7 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition) public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, LogLevel.Debug); public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, LogLevel.Debug); public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, LogLevel.Debug); - public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Trace); public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, LogLevel.Information); public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, LogLevel.Information); public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, LogLevel.Information); diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs index 5a5306fe2ddf..7335284574b0 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -7,6 +7,7 @@ using System.Data; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Commands; using Microsoft.DotNet.Cli.Commands.Build; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Test; @@ -16,8 +17,6 @@ namespace Microsoft.DotNet.Watch; internal sealed class CommandLineOptions { - public const string DefaultCommand = "run"; - private static readonly ImmutableArray s_binaryLogOptionNames = ["-bl", "/bl", "-binaryLogger", "--binaryLogger", "/binaryLogger"]; public bool List { get; init; } @@ -38,9 +37,9 @@ internal sealed class CommandLineOptions /// public required IReadOnlyList BuildArguments { get; init; } - public string? ExplicitCommand { get; init; } + public required string Command { get; init; } - public string Command => ExplicitCommand ?? DefaultCommand; + public required bool IsExplicitCommand { get; init; } public static CommandLineOptions? Parse(IReadOnlyList args, ILogger logger, TextWriter output, out int errorCode) { @@ -67,8 +66,7 @@ internal sealed class CommandLineOptions } // determine subcommand: - var explicitCommand = TryGetSubcommand(parseResult); - var command = explicitCommand ?? RunCommandParser.GetCommand(); + var command = GetSubcommand(parseResult, out bool isExplicitCommand); var buildOptions = command.Options.Where(o => o.ForwardingFunction is not null); foreach (var buildOption in buildOptions) @@ -111,7 +109,7 @@ internal sealed class CommandLineOptions var commandArguments = GetCommandArguments( parseResult, command, - explicitCommand, + isExplicitCommand, out var binLogToken, out var binLogPath); @@ -143,7 +141,8 @@ internal sealed class CommandLineOptions }, CommandArguments = commandArguments, - ExplicitCommand = explicitCommand?.Name, + Command = command.Name, + IsExplicitCommand = isExplicitCommand, ProjectPath = projectValue, LaunchProfileName = parseResult.GetValue(definition.LaunchProfileOption), @@ -176,7 +175,7 @@ internal sealed class CommandLineOptions private static IReadOnlyList GetCommandArguments( ParseResult parseResult, Command command, - Command? explicitCommand, + bool isExplicitCommand, out string? binLogToken, out string? binLogPath) { @@ -250,7 +249,7 @@ private static IReadOnlyList GetCommandArguments( if (i < unmatchedTokensBeforeDashDash) { - if (!seenCommand && token == explicitCommand?.Name) + if (!seenCommand && isExplicitCommand && token == command.Name) { seenCommand = true; continue; @@ -293,24 +292,27 @@ private static string GetOptionNameToForward(OptionResult optionResult) // For those that do not, use the Option's Name instead. => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; - private static Command? TryGetSubcommand(ParseResult parseResult) + private static Command GetSubcommand(ParseResult parseResult, out bool isExplicit) { // Assuming that all tokens after "--" are unmatched: var dashDashIndex = IndexOf(parseResult.Tokens, t => t.Value == "--"); var unmatchedTokensBeforeDashDash = parseResult.UnmatchedTokens.Count - (dashDashIndex >= 0 ? parseResult.Tokens.Count - dashDashIndex - 1 : 0); - var knownCommandsByName = Parser.Subcommands.ToDictionary(keySelector: c => c.Name, elementSelector: c => c); + var dotnetDefinition = new DotNetCommandDefinition(); + var knownCommandsByName = dotnetDefinition.Subcommands.ToDictionary(keySelector: c => c.Name, elementSelector: c => c); for (int i = 0; i < unmatchedTokensBeforeDashDash; i++) { // command token can't follow "--" if (knownCommandsByName.TryGetValue(parseResult.UnmatchedTokens[i], out var explicitCommand)) { + isExplicit = true; return explicitCommand; } } - return null; + isExplicit = false; + return dotnetDefinition.RunCommand; } private static bool ReportErrors(ParseResult parseResult, ILogger logger) diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index 384df13ae5cc..476108f319c7 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -46,6 +46,9 @@ public static async Task Main(string[] args) var environmentOptions = EnvironmentOptions.FromEnvironment(processPath); + // msbuild tasks depend on host path variable: + Environment.SetEnvironmentVariable(EnvironmentVariables.Names.DotnetHostPath, environmentOptions.MuxerPath); + var program = TryCreate( args, new PhysicalConsole(environmentOptions.TestFlags), diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json index 9e28729eb807..b60b37dc0855 100644 --- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json +++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json @@ -2,14 +2,15 @@ "profiles": { "dotnet-watch": { "commandName": "Project", - "commandLineArgs": "--verbose -bl", - "workingDirectory": "C:\\bugs\\9756\\aspire-watch-start-issue\\Aspire.AppHost", + "commandLineArgs": "-bl", + "workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm", "environmentVariables": { "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)", "DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000", "DCP_IDE_NOTIFICATION_TIMEOUT_SECONDS": "100000", "DCP_IDE_NOTIFICATION_KEEPALIVE_SECONDS": "100000", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1" + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1", + "DOTNET_CLI_CONTEXT_VERBOSE": "true" } } } diff --git a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs index 32f8f2a0d58d..52f7f6d8985b 100644 --- a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs +++ b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Text.Json; using Microsoft.Build.Graph; +using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch @@ -96,7 +97,8 @@ internal sealed class EvaluationResult(IReadOnlyDictionary fil foreach (var staticFile in projectItems.StaticFiles) { - AddFile(staticFile.FilePath, staticFile.StaticWebAssetPath); + // that target adds items with "wwwroot/" prefix: + AddFile(staticFile.FilePath, staticFile.StaticWebAssetPath?["wwwroot/".Length..]); } void AddFile(string filePath, string? staticWebAssetPath) @@ -107,7 +109,7 @@ void AddFile(string filePath, string? staticWebAssetPath) { FilePath = filePath, ContainingProjectPaths = [projectPath], - StaticWebAssetPath = staticWebAssetPath, + StaticWebAssetRelativeUrl = staticWebAssetPath, }); } else if (!existingFile.ContainingProjectPaths.Contains(projectPath)) @@ -161,7 +163,7 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath) // Set dotnet-watch reserved properties after the user specified propeties, // so that the former take precedence. - if (EnvironmentOptions.SuppressHandlingStaticContentFiles) + if (EnvironmentOptions.SuppressHandlingStaticWebAssets) { arguments.Add("/p:DotNetWatchContentFiles=false"); } diff --git a/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs similarity index 92% rename from src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs rename to src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs index d20603095ab3..dddba6cb2d41 100644 --- a/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs @@ -19,7 +19,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f { var file = files[i].Item; - if (file.StaticWebAssetPath is null) + if (file.StaticWebAssetRelativeUrl is null) { allFilesHandled = false; continue; @@ -46,7 +46,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f refreshRequests.Add(refreshServer, filesPerServer = []); } - filesPerServer.Add(file.StaticWebAssetPath); + filesPerServer.Add(StaticWebAsset.WebRoot + "/" + file.StaticWebAssetRelativeUrl); } else if (projectsWithoutRefreshServer.Add(projectNode)) { @@ -65,7 +65,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f await Task.WhenAll(tasks).WaitAsync(cancellationToken); - logger.Log(MessageDescriptor.HotReloadOfStaticAssetsSucceeded); + logger.Log(MessageDescriptor.StaticAssetsReloaded); return allFilesHandled; } diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj index 6ee14bf8ee71..5d5968b4f949 100644 --- a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj +++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj @@ -28,14 +28,12 @@ - - + - - + all Content diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetFiles.cs similarity index 82% rename from src/Cli/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetFiles.cs index 3174bbcd926f..85adda5135bb 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetFiles.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NET + using System.Reflection; namespace Microsoft.DotNet.Cli.Utils; -internal static class DotnetFiles +public static class DotnetFiles { private static string SdkRootFolder => AppContext.BaseDirectory; @@ -17,5 +19,7 @@ internal static class DotnetFiles /// public static string VersionFile => Path.GetFullPath(Path.Combine(SdkRootFolder, ".version")); - internal static DotnetVersionFile VersionFileObject => s_versionFileObject.Value; + public static DotnetVersionFile VersionFileObject => s_versionFileObject.Value; } + +#endif diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetVersionFile.cs similarity index 97% rename from src/Cli/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetVersionFile.cs index 2794142d48f6..751a9e2dfad2 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/DotnetVersionFile.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. +#if NET + namespace Microsoft.DotNet.Cli.Utils; -internal class DotnetVersionFile +public class DotnetVersionFile { public bool Exists { get; init; } @@ -67,3 +69,5 @@ public DotnetVersionFile(string versionFilePath) } } } + +#endif diff --git a/src/Cli/Microsoft.DotNet.Cli.CoreUtils/EnvironmentVariableParser.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/EnvironmentVariableParser.cs new file mode 100644 index 000000000000..622d4073b2a2 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/EnvironmentVariableParser.cs @@ -0,0 +1,28 @@ +// 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.Cli; + +public static class EnvironmentVariableParser +{ + public static bool ParseBool(string? str, bool defaultValue) + { + if (str is "1" || + string.Equals(str, "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(str, "yes", StringComparison.OrdinalIgnoreCase) || + string.Equals(str, "on", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (str is "0" || + string.Equals(str, "false", StringComparison.OrdinalIgnoreCase) || + string.Equals(str, "no", StringComparison.OrdinalIgnoreCase) || + string.Equals(str, "off", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return defaultValue; + } +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/ExceptionExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/ExceptionExtensions.cs similarity index 65% rename from src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/ExceptionExtensions.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/ExceptionExtensions.cs index 8ea857636334..f997c2a87caa 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/ExceptionExtensions.cs +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/ExceptionExtensions.cs @@ -1,22 +1,17 @@ // 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.Cli.Utils.Extensions; +namespace Microsoft.DotNet.Cli.Utils; -internal static class ExceptionExtensions +public static class ExceptionExtensions { public static TException DisplayAsError(this TException exception) - where TException : Exception + where TException : Exception { exception.Data.Add(CLI_User_Displayed_Exception, true); return exception; } - public static void ReportAsWarning(this Exception e) - { - Reporter.Verbose.WriteLine($"Warning: Ignoring exception: {e.ToString().Yellow()}"); - } - public static bool ShouldBeDisplayedAsError(this Exception e) => e.Data.Contains(CLI_User_Displayed_Exception); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/GracefulException.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/GracefulException.cs similarity index 96% rename from src/Cli/Microsoft.DotNet.Cli.Utils/GracefulException.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/GracefulException.cs index f1a9c99b47ee..8064609faa06 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/GracefulException.cs +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/GracefulException.cs @@ -1,8 +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 Microsoft.DotNet.Cli.Utils.Extensions; - namespace Microsoft.DotNet.Cli.Utils; public class GracefulException : Exception diff --git a/src/Cli/Microsoft.DotNet.Cli.CoreUtils/Microsoft.DotNet.Cli.CoreUtils.csproj b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/Microsoft.DotNet.Cli.CoreUtils.csproj new file mode 100644 index 000000000000..7ca17824ecb9 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/Microsoft.DotNet.Cli.CoreUtils.csproj @@ -0,0 +1,9 @@ + + + $(SdkTargetFramework);net472 + enable + true + enable + MicrosoftAspNetCore + + diff --git a/src/Cli/Microsoft.DotNet.Cli.CoreUtils/PathUtilities.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/PathUtilities.cs new file mode 100644 index 000000000000..0012b8b40b7b --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/PathUtilities.cs @@ -0,0 +1,24 @@ +// 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.Cli; + +public static class PathUtilities +{ + public static string EnsureTrailingSlash(string path) + => EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + + public static string EnsureTrailingForwardSlash(string path) + => EnsureTrailingCharacter(path, '/'); + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/Product.cs similarity index 85% rename from src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/Product.cs index 3ec19eccd7e4..6dc0a13d6d79 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs +++ b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/Product.cs @@ -1,15 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Diagnostics; +#if NET + using System.Reflection; namespace Microsoft.DotNet.Cli.Utils; public static class Product { - public static string LongName => LocalizableStrings.DotNetSdkInfo; public static readonly string Version; public static readonly string TargetFrameworkVersion = "11.0"; @@ -22,3 +21,5 @@ static Product() ?? string.Empty; } } + +#endif diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/VerbosityOptions.cs b/src/Cli/Microsoft.DotNet.Cli.CoreUtils/VerbosityOptions.cs similarity index 100% rename from src/Cli/Microsoft.DotNet.Cli.Utils/VerbosityOptions.cs rename to src/Cli/Microsoft.DotNet.Cli.CoreUtils/VerbosityOptions.cs diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/CommandDefinitionStrings.resx b/src/Cli/Microsoft.DotNet.Cli.Definitions/CommandDefinitionStrings.resx new file mode 100644 index 000000000000..7a348d2e5085 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/CommandDefinitionStrings.resx @@ -0,0 +1,1497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Do not display the startup banner or the copyright message. + + + Package reference + + + Required command was not provided. + + + PROJECT | FILE + + + The project file to operate on. If a file is not specified, the command will search the current directory for one. + + + The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. + + + The file-based app to operate on. + + + FRAMEWORK + + + Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. + + + Set the value of the $(VersionSuffix) property to use when building the project. + + + Do not restore the project before building. + + + LEVEL + + + FRAMEWORK + + + RUNTIME_IDENTIFIER + + + CONFIGURATION + + + VERSION_SUFFIX + + + The project or solution file to operate on. If a file is not specified, the command will search the current directory for one. + + + PROJECT | SOLUTION + + + The project or solution or C# (file-based program) file to operate on. If a file is not specified, the command will search the current directory for a project or solution. + + + PROJECT | SOLUTION | FILE + + + Allows the command to stop and wait for user input or action (for example to complete authentication). + + + Allows prerelease packages to be installed. + + + The target architecture. + + + The target operating system. + + + Resolving the current runtime identifier failed. + + + Specifying both the `-r|--runtime` and `-a|--arch` options is not supported. + + + Specifying both the `-r|--runtime` and `-os` options is not supported. + + + Sets the value of an environment variable. +Creates the variable if it does not exist, overrides if it does. +This argument can be specified multiple times to provide multiple variables. + +Examples: +-e VARIABLE=abc +-e VARIABLE="value with spaces" +-e VARIABLE="value;seperated with;semicolons" +-e VAR1=abc -e VAR2=def -e VAR3=ghi + + + + Sets the value of an environment variable. +Creates the variable if it does not exist, overrides if it does. +This will force the tests to be run in an isolated process. +This argument can be specified multiple times to provide multiple variables. + +Examples: +-e VARIABLE=abc +-e VARIABLE="value with spaces" +-e VARIABLE="value;seperated with;semicolons" +-e VAR1=abc -e VAR2=def -e VAR3=ghi + + + + NAME="VALUE" + + + Incorrectly formatted environment variables: {0} + + + Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine. +The default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified. + + + Publish your application as a framework dependent application. A compatible .NET runtime must be installed on the target machine to run your application. + + + The '--self-contained' and '--no-self-contained' options cannot be used together. + {Locked="--self-contained"}{Locked="--no-self-contained"} + + + Force the command to ignore any persistent build servers. + + + ARCH + + + OS + + + The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. + + + ARTIFACTS_DIR + + + Unrecognized command or argument '{0}' + + + Enable diagnostic output. + + + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + + + Package reference id and version must not be null. + + + Invalid version string: {0} + + + Accept all confirmation prompts using "yes." + + + Add one or more projects to a solution file. + + + The paths to the projects to add to the solution. + + + PROJECT_PATH + + + The destination solution folder path to add the projects to. + + + Only update advertising manifests. + + + Allow package downgrade when installing a .NET tool package. + + + .NET Builder + + + The configuration to use for building the project. The default for most projects is 'Debug'. + + + The target framework to build for. The target framework must also be specified in the project file. + + + The output directory to place built artifacts in. + + + The target runtime to build for. + + + Interact with servers started from a build. + + + Shuts down build servers that are started from dotnet. By default, all servers are shut down. + + + Cannot specify both the {0} and {1} arguments. + + + Causes clean to remove and uninstall all workload components from all SDK versions. + + + .NET Clean Command + + + OUTPUT_DIR + + + The directory containing the build artifacts to clean. + + + The configuration to clean for. The default for most projects is 'Debug'. + + + The target framework to clean for. The target framework must also be specified in the project file. + + + The target runtime to clean for. + + + Removes artifacts created for file-based apps + + + Determines changes without actually modifying the file system + + + How many days an artifact folder needs to be unused in order to be removed + + + Enables collecting crash dump on expected as well as unexpected testhost exit. + + + Runs the tests in blame mode and collects a crash dump when the test host exits unexpectedly. This option depends on the version of .NET used, the type of error, and the operating system. + +For exceptions in managed code, a dump will be automatically collected on .NET 5.0 and later versions. It will generate a dump for testhost or any child process that also ran on .NET 5.0 and crashed. Crashes in native code will not generate a dump. This option works on Windows, macOS, and Linux. + +Crash dumps in native code, or when targetting .NET Framework, or .NET Core 3.1 and earlier versions, can only be collected on Windows, by using Procdump. A directory that contains procdump.exe and procdump64.exe must be in the PATH or PROCDUMP_PATH environment variable. + +The tools can be downloaded here: https://docs.microsoft.com/sysinternals/downloads/procdump + +To collect a crash dump from a native application running on .NET 5.0 or later, the usage of Procdump can be forced by setting the VSTEST_DUMP_FORCEPROCDUMP environment variable to 1. + +Implies --blame. + + + The type of crash dump to be collected. Supported values are full (default) and mini. Implies --blame-crash. + + + Runs the tests in blame mode. This option is helpful in isolating problematic tests that cause the test host to crash or hang, but it does not create a memory dump by default. + +When a crash is detected, it creates an sequence file in TestResults/guid/guid_Sequence.xml that captures the order of tests that were run before the crash. + +Based on the additional settings, hang dump or crash dump can also be collected. + +Example: + Timeout the test run when test takes more than the default timeout of 1 hour, and collect crash dump when the test host exits unexpectedly. + (Crash dumps require additional setup, see below.) + dotnet test --blame-hang --blame-crash +Example: + Timeout the test run when a test takes more than 20 minutes and collect hang dump. + dotnet test --blame-hang-timeout 20min + + + + Run the tests in blame mode and enables collecting hang dump when test exceeds the given timeout. + + + The type of crash dump to be collected. The supported values are full (default), mini, and none. When 'none' is used then test host is terminated on timeout, but no dump is collected. Implies --blame-hang. + + + Per-test timeout, after which hang dump is triggered and the testhost process is terminated. Default is 1h. +The timeout value is specified in the following format: 1.5h / 90m / 5400s / 5400000ms. When no unit is used (e.g. 5400000), the value is assumed to be in milliseconds. +When used together with data driven tests, the timeout behavior depends on the test adapter used. For xUnit, NUnit and MSTest 2.2.4+ the timeout is renewed after every test case, +For MSTest before 2.2.4, the timeout is used for all testcases. + + + The friendly name of the data collector to use for the test run. + More info here: https://aka.ms/vstest-collect + + + DATA_COLLECTOR_NAME + + + CONFIG_FILE + + + The path to the NuGet config file to use. Requires the '--outdated', '--deprecated' or '--vulnerable' option. + + + FILE + + + The NuGet configuration file to use. + + + Use current runtime as the target runtime. + + + Lists packages that have been deprecated. Cannot be combined with '--vulnerable' or '--outdated' options. + + + Prevent restoring multiple projects in parallel. + + + EXPRESSION + + + Path to the file-based program. + + + Force conversion even if there are malformed directives. + + + Force all dependencies to be resolved even if the last restore was successful. +This is equivalent to deleting project.assets.json. + + + Specifies the output format type for the list packages command. + + + Consider only the packages with a matching major version number when searching for newer packages. Requires the '--outdated' option. + + + Consider only the packages with a matching major and minor version numbers when searching for newer packages. Requires the '--outdated' option. + + + Treat package source failures as warnings. + + + Include PDBs and source files. Source files go into the 'src' folder in the resulting nuget package. + + + Include packages with symbols in addition to regular packages in output directory. + + + List the discovered tests instead of running the tests. + + + Don't allow updating project lock file. + + + LOCK_FILE_PATH + + + Output location where project lock file is written. By default, this is 'PROJECT_ROOT\packages.lock.json'. + + + The logger to use for test results. + Examples: + Log in trx format using a unique file name: --logger trx + Log in trx format using the specified file name: --logger "trx;LogFileName=<TestResults.trx>" + See https://aka.ms/vstest-report for more information on logger arguments. + + + LOGGER + + + The max number of test modules that can run in parallel. + + + Disable ANSI output. + + + Do not build the project before testing. Implies --no-restore. + + + Do not build the project before packing. Implies --no-restore. + + + Do not cache packages and http requests. + + + Do not restore project-to-project references and only restore the specified project. + + + Disable Http Caching for packages. + + + Disable progress reporting. + + + NUMBER + + + Lists packages that have newer versions. Cannot be combined with '--deprecated' or '--vulnerable' options. + + + The output directory to place built artifacts in. + + + Specifies the version of machine-readable output. Requires the '--format json' option. + + + Do not restore before running the command. + + + PACKAGE_NAME + + + PACKAGE_DIR + + + The directory to restore packages to. + + + PACKAGES_DIR + + + The directory to restore packages to. + + + LOG_FILE + + + Enable verbose logging to the specified file. + + + RESULTS_DIR + + + Consider packages with prerelease versions when searching for newer packages. Requires the '--outdated' option. + + + Defines the path of the project file to run. Use path to the project file, or path to the directory containing the project file. If not specified, it defaults to the current directory. + + + Defines the path of the project or solution file to test. Use path to the project file, or path to the directory containing the project file. If not specified, it defaults to the current directory. + + + PROJECT_OR_SOLUTION_PATH + + + Forces restore to reevaluate all dependencies even if a lock file already exists. + + + The directory where the test results will be placed. +The specified directory will be created if it does not exist. + + + Specifies a testconfig.json file. + + + CONFIG_FILE + + + Output directory of the diagnostic logging. +If not specified the file will be generated inside the default 'TestResults' directory. + + + DIAGNOSTIC_DIR + + + ROOT_PATH + + + RUNTIME_IDENTIFIER + + + The target runtime to restore packages for. + + + Set the serviceable flag in the package. See https://aka.ms/nupkgservicing for more information. + + + The settings file to use when running tests. + + + SETTINGS_FILE + + + Defines the path of the solution file to test. Use path to the solution file, or path to the directory containing the solution file. If not specified, it defaults to the current directory. + + + SOLUTION_PATH + + + SOURCE + + + The NuGet package source to use for the restore. + + + ADAPTER_PATH + + + The path to the custom adapters to use for the test run. + + + Run tests that match the given expression. + Examples: + Run tests with priority set to 1: --filter "Priority = 1" + Run a test with the specified full name: --filter "FullyQualifiedName=Namespace.ClassName.MethodName" + Run tests that contain the specified name: --filter "FullyQualifiedName~Namespace.Class" + See https://aka.ms/vstest-filtering for more information on filtering support. + + + + EXPRESSION + + + Run tests for the specified test modules. + + + The test modules have the specified root directory. + + + Verbosity of test output. + + + Lists transitive and top-level packages. + + + Test runner '{0}' is not supported. + + + Enables project lock file to be generated and used with restore. + + + VERSION + + + The version of the package to add. + + + Lists packages that have known vulnerabilities. Cannot be combined with '--deprecated' or '--outdated' options. + + + The SDK command to launch online help for. + + + COMMAND_NAME + + + The command name of the tool to run. + + + COMMAND_NAME + + + The name of the launch profile (if any) to use when launching the application. + + + LAUNCH_PROFILE + + + Do not build the project before running. Implies --no-restore. + {Locked="--no-restore"} + + + Skip up to date checks and always build the program before running. + + + Do not use arguments specified in launch profile to run the application. + + + Do not attempt to use launchSettings.json or [app].run.json to configure the application. + {Locked="launchSettings.json"}{Locked=".run.json"} + + + The device identifier to use for running the application. + + + DEVICE + + + List available devices for running the application. + + + PROJECT_PATH + + + The path to the file-based app to run (can be also passed as the first argument if there is no project in the current directory). + + + FILE_PATH + + + ConfigFile + + + The NuGet configuration file. If specified, only the settings from this file will be used. If not specified, the hierarchy of configuration files from the current directory will be used. For more information, see https://docs.microsoft.com/nuget/consume-packages/configuring-nuget-behavior + + + DUMP_TYPE + + + Create a tool manifest if one isn't found during tool installation. For information on how manifests are located, see https://aka.ms/dotnet/tools/create-manifest-if-needed + + + Use current runtime as the target runtime. + + + Show detail result of the query. + + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + DIRECTORY + + + Download packages needed to install a workload to a folder that can be used for offline installation. + + + Require that the search term exactly match the name of the package. Causes `--take` and `--skip` options to be ignored. + + + Format + + + Format the output accordingly. Either `table`, or `json`. The default value is `table`. + + + Changes the format of outputted workload versions. Can take 'json' or 'list' + + + FRAMEWORK_VERSION + + + The Microsoft.NETCore.App package version that will be used to run the assemblies. + + + DIRECTORY + + + Complete the operation from cache (offline). + + + Update workloads to a previous version specified by the argument. Use the 'dotnet workload history' to see available workload history records. + + + Include workloads installed with earlier SDK versions in update. + + + Update workloads based on specified rollback definition file. + + + DUMP_TYPE + + + TIMESPAN + + + .NET CLI help utility + + + Update to the workload versions specified in the history without changing which workloads are installed. Currently installed workloads will be updated to match the specified history version. + + + Allow prerelease workload manifests. + + + Place project in root of the solution, rather than creating a solution folder. + + + WORKING_DIR + + + The working directory used by the command to execute. + + + List all projects in a solution file. + + + Display solution folder paths. + + + MANIFEST + + + The path to a target manifest file that contains the list of packages to be excluded from the publish step. + + + Generate a .slnx file from a .sln file. + + + Shut down the MSBuild build server. + + + .NET Add Command + + + List references or packages of a .NET project. + + + .NET Remove Command + + + Do not build the project before publishing. Implies --no-restore. + + + Do not build project-to-project references and only build the specified project. + + + Do not use incremental building. + + + Specifying the tool manifest option (--tool-manifest) is only valid with the local option (--local or the default). + + + Options '--outdated', '--deprecated' and '--vulnerable' cannot be combined. + + + OUTPUT_DIR + + + Add a NuGet package reference to the project. + + + FRAMEWORK + + + Add the reference only when targeting a specific framework. + + + Add the reference without performing restore preview and compatibility check. + + + SOURCE + + + The NuGet package source to use during the restore. + + + List all package references of the project or solution. + + + FRAMEWORK | FRAMEWORK\RID + + + Chooses a framework to show its packages. Use the option multiple times for multiple frameworks. + + + SOURCE + + + The NuGet sources to use when searching for newer packages. Requires the '--outdated', '--deprecated' or '--vulnerable' option. + + + Remove a NuGet package reference from the project. + + + The package reference to remove. + + + Searches one or more package sources for packages that match a search term. If no sources are specified, all sources defined in the NuGet.Config are used. + + + Include prerelease packages. + + + SearchTerm + + + Search term to filter package names, descriptions, and tags. Used as a literal value. Example: `dotnet package search some.package`. See also `--exact-match`. + + + Skip + + + Number of results to skip, to allow pagination. Default 0. + + + Take + + + Number of results to return. Default 20. + + + .NET Core NuGet Package Packer + + + OUTPUT_DIR + + + The output directory to place built packages in. + + + The configuration to use for building the package. The default is 'Release'. + + + Only print the list of links to download without downloading. + + + 'dotnet workload search version' has three functions depending on its argument: + 1. If no argument is specified, it outputs a list of the latest released workload versions from this feature band. Takes the --take option to specify how many to provide and --format to alter the format. + Example: + dotnet workload search version --take 2 --format json + [{"workloadVersion":"9.0.201"},{"workloadVersion":"9.0.200.1"}] + 2. If a workload version is provided as an argument, it outputs a table of various workloads and their versions for the specified workload version. Takes the --format option to alter the output format. + Example: + dotnet workload search version 9.0.201 + Workload manifest ID Manifest feature band Manifest Version + ------------------------------------------------------------------------------------------------ + microsoft.net.workload.emscripten.current 9.0.100-rc.1 9.0.0-rc.1.24430.3 + microsoft.net.workload.emscripten.net6 9.0.100-rc.1 9.0.0-rc.1.24430.3 + microsoft.net.workload.emscripten.net7 9.0.100-rc.1 9.0.0-rc.1.24430.3 + microsoft.net.workload.emscripten.net8 9.0.100-rc.1 9.0.0-rc.1.24430.3 + microsoft.net.sdk.android 9.0.100-rc.1 35.0.0-rc.1.80 + microsoft.net.sdk.ios 9.0.100-rc.1 17.5.9270-net9-rc1 + microsoft.net.sdk.maccatalyst 9.0.100-rc.1 17.5.9270-net9-rc1 + microsoft.net.sdk.macos 9.0.100-rc.1 14.5.9270-net9-rc1 + microsoft.net.sdk.maui 9.0.100-rc.1 9.0.0-rc.1.24453.9 + microsoft.net.sdk.tvos 9.0.100-rc.1 17.5.9270-net9-rc1 + microsoft.net.workload.mono.toolchain.current 9.0.100-rc.1 9.0.0-rc.1.24431.7 + microsoft.net.workload.mono.toolchain.net6 9.0.100-rc.1 9.0.0-rc.1.24431.7 + microsoft.net.workload.mono.toolchain.net7 9.0.100-rc.1 9.0.0-rc.1.24431.7 + microsoft.net.workload.mono.toolchain.net8 9.0.100-rc.1 9.0.0-rc.1.24431.7 + 3. If one or more workloads are provided along with their versions (by joining them with the '@' character), it outputs workload versions that match the provided versions. Takes the --take option to specify how many to provide and --format to alter the format. + Example: + dotnet workload search version maui@9.0.0-rc.1.24453.9 ios@17.5.9270-net9-rc1 + 9.0.201 + + {Locked="--take"} {Locked="--format"} {Locked="dotnet workload search version"} {Locked="workloadVersion"} + + + Convert a file-based program to a project-based program. + + + Determines changes without actually modifying the file system + + + PROJECT_MANIFEST + + + The XML file that contains the list of packages to be stored. + + + Publisher for the .NET Platform + + + The configuration to publish for. The default is 'Release' for NET 8.0 projects and above, but 'Debug' for older projects. + + + The target framework to publish for. The target framework has to be specified in the project file. + + + OUTPUT_DIR + + + The output directory to place the published artifacts in. + + + The target runtime to publish for. This is used when creating a self-contained deployment. +The default is to publish a framework-dependent application. + + + Shut down the Razor build server. + + + Add a project-to-project reference to the project. + + + Add the reference only when targeting a specific framework. + + + The paths to the projects to add as references. + + + PROJECT_PATH + + + List all project-to-project references of the project. + + + Remove a project-to-project reference from the project. + + + Remove the reference only when targeting a specific framework. + + + The paths to the referenced projects to remove. + + + PROJECT_PATH + + + Remove one or more projects from a solution file. + + + The project paths or names to remove from the solution. + + + PROJECT_PATH + + + .NET dependency restorer + + + Allow a .NET tool to roll forward to newer versions of the .NET runtime if the runtime it targets isn't installed. + + + .NET Run Command + + + The configuration to run for. The default for most projects is 'Debug'. + + + The target framework to run for. The target framework must also be specified in the project file. + + + The target runtime to run for. + + + .NET SDK Command + + + .NET SDK Check Command + + + Skip updating the workload manifests. + + + Skip the optimization phase. + + + Skip signature verification of workload packages and installers. + + + Skip creating symbol files which can be used for profiling the optimized assemblies. + + + .NET modify solution file command + + + The solution file to operate on. If not specified, the command will search the current directory for one. + + + SLN_FILE + + + Source + + + The package source to search. You can pass multiple `--source` options to search multiple package sources. Example: `--source https://api.nuget.org/v3/index.json`. + + + Stores the specified assemblies for the .NET Platform. By default, these will be optimized for the target runtime and framework. + + + The target framework to store packages for. The target framework has to be specified in the project file. + + + OUTPUT_DIR + + + The output directory to store the given assemblies in. + + + The target runtime to store packages for. + + + Specify a temporary directory for this command to download and extract NuGet packages (must be secure). + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + Run test(s), without displaying Microsoft Testplatform banner + + + OUTPUT_DIR + + + The configuration to use for running tests. The default for most projects is 'Debug'. + + + The target framework to run tests for. The target framework must also be specified in the project file. + + + The target runtime to test for. + + + Install or work with tools that extend the .NET experience. + + + Add an additional NuGet package source to use during installation. + + + ADDSOURCE + + + Install global or local tool. Local tools are added to manifest and restored. + + + The NuGet configuration file to use. + + + FILE + + + The target framework to install the tool for. + + + FRAMEWORK + + + Install the tool for the current user. + + + Install the tool and add to the local tool manifest (default). + + + Path to the manifest file. + + + PATH + + + Replace all NuGet package sources to use during installation with these. + + + SOURCE + + + The directory where the tool will be installed. The directory will be created if it does not exist. + + + PATH + + + The version of the tool package to install. + + + VERSION + + + List tools installed globally or locally. + + + The output format for the list of tools. + + + List tools installed for the current user. + + + List the tools installed in the local tool manifest. + + + The NuGet Package Id of the tool to list + + + PACKAGE_ID + + + The directory containing the tools to list. + + + Restore tools defined in the local tool manifest. + + + Path to the manifest file. + + + Run a local tool. Note that this command cannot be used to run a global tool. + + + Arguments forwarded to the tool + + + Search dotnet tools in nuget.org + + + Include pre-release packages. + + + SEARCH_TERM + + + Search term from package id or package description. Require at least one character. + + + Skip + + + The number of results to skip, for pagination. + + + Take + + + The number of results to return, for pagination. + + + Uninstall a global tool or local tool. + + + Uninstall the tool from the current user's tools directory. + + + Uninstall the tool and remove it from the local tool manifest. + + + Path to the manifest file. + + + The directory containing the tool to uninstall. + + + Update a global or local tool. + + + Update all tools. + + + Controls whether updates should look for workload sets or the latest version of each individual manifest. + + + Cannot specify --version when the package argument already contains a version. + {Locked="--version"} + + + Shut down the VB/C# compiler build server. + + + Verbosity + + + Display this amount of details in the output: `normal`, `minimal`, `detailed`. The default is `normal` + + + Removes workload components that may have been left behind from previous updates and uninstallations. + + + Install or work with workloads that extend the .NET experience. + + + Modify or display workload configuration values. +To display a value, specify the corresponding command-line option without providing a value. For example: "dotnet workload config --update-mode" + + + Start the elevated server process to facilitate MSI based installations. + + + Shows a history of workload installation actions. + + + The NuGet package ID of the workload to install. + + + WORKLOAD_ID + + + The text to search for in the IDs and descriptions of available workloads. + + + SEARCH_STRING + + + Display information about installed workloads. + + + Install one or more workloads. + + + The NuGet configuration file to use. + + + FILE + + + The NuGet package source to use during the restore. To specify multiple sources, repeat the option. + + + SOURCE + + + The version of the SDK. + + + VERSION + + + List workloads available. + + + Repair workload installations. + + + Restore workloads required for a project. + + + Search for available workloads. + + + A workload version to display or one or more workloads and their versions joined by the '@' character. + + + Uninstall one or more workloads. + + + Update all installed workloads. + + + WORKLOAD_VERSION + + + Output workload manifest versions associated with the provided workload version. + + + Display the currently installed workload version. + + + Recursively add projects' ReferencedProjects to solution + + + Executes a tool from source without permanently installing it. + + + The version of the package to create + + + VERSION + + + Specifies the minimum number of tests that are expected to run. + + + Location to place the generated output. + + + Creates an alias for instantiate command with a certain set of arguments. + + + Displays defined aliases. + + + Creates or displays defined aliases. + + + + Provides the details for specified template package. + The command checks if the package is installed locally, if it was not found, it searches the configured NuGet feeds. + + + NuGet package ID or path to folder or NuGet package to install. +To install the NuGet package of certain version, use <package ID>::<version>. + + + + Installs a template package. + + + A short name of the template to create. + + + Template specific options to use. + + + Instantiates a template with given short name. An alias of 'dotnet new <template name>'. + do not translate 'dotnet new <template name>' + + + Checks the currently installed template packages for updates. + + + If specified, only the templates matching the name will be shown. + + + Lists templates containing the specified template name. If no name is specified, lists all templates. + + + Template Instantiation Commands for .NET CLI. + + + If specified, only the templates matching the name will be shown. + + + Searches for the templates on NuGet.org. + + + NuGet package ID (without version) or path to folder to uninstall. +If command is specified without the argument, it lists all the template packages installed. + + + Uninstalls a template package. + + + Checks the currently installed template packages for update, and install the updates. + + + Only checks for updates and display the template packages to be updated without applying update. + + + Disables checking if the template meets the constraints to be run. + + + Specifies a NuGet source to use. + + + Filters the templates based on the template author. + + + Filters the templates based on baseline defined in the template. + + + Specifies the columns to display in the output. + + + Displays all columns in the output. + + + Allows to pause execution in order to attach to the process for debug purposes. + + + Sets custom settings location. + + + If specified, removes the template cache prior to command execution. + + + If specified, resets the settings prior to command execution. + + + If specified, shows the template engine config prior to command execution. + + + If specified, the settings will be not preserved on file system. + + + Allows installing template packages from the specified sources even if they would override a template package from another source. + + + Allows the command to stop and wait for user input or action (for example to complete authentication). + + + Filters templates based on language. + + + Filters the templates based on NuGet package ID. + + + The project that should be used for context evaluation. + + + Filters the templates based on the tag. + + + Filters templates based on available types. Predefined values are "project" and "item". + project and item should not be translated + + + Displays a summary of what would happen if the given command line were run if it would result in a template creation. + + + Forces content to be generated even if it would change existing files. + + + The name for the output being created. If no name is specified, the name of the output directory is used. + + + Disables checking for the template package updates when instantiating a template. + + + Unrecognized command or argument(s): {0}. + {0} - wrong token or comma-separated tokens (if multiple). Each token is enclosed with single quotes: 'token'. + + + Package identifier + + + If present, prevents templates bundled in the SDK from being presented. + + + Disables evaluating project context using MSBuild. + + + Sets the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], and diag[nostic]. + + + Enables diagnostic output. + + + Display the command schema as JSON. + + diff --git a/src/Cli/dotnet/Commands/Build/BuildCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Build/BuildCommandDefinition.cs similarity index 83% rename from src/Cli/dotnet/Commands/Build/BuildCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Build/BuildCommandDefinition.cs index e411351aac51..b2fee65c8862 100644 --- a/src/Cli/dotnet/Commands/Build/BuildCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Build/BuildCommandDefinition.cs @@ -4,7 +4,6 @@ using System.CommandLine; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Restore; -using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Build; @@ -12,9 +11,9 @@ internal sealed class BuildCommandDefinition : Command { private const string Link = "https://aka.ms/dotnet-build"; - public readonly Argument SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName) + public readonly Argument SlnOrProjectOrFileArgument = new(CommandDefinitionStrings.SolutionOrProjectOrFileArgumentName) { - Description = CliStrings.SolutionOrProjectOrFileArgumentDescription, + Description = CommandDefinitionStrings.SolutionOrProjectOrFileArgumentDescription, Arity = ArgumentArity.ZeroOrMore }; @@ -22,19 +21,19 @@ internal sealed class BuildCommandDefinition : Command public readonly Option OutputOption = new Option("--output", "-o") { - Description = CliCommandStrings.BuildOutputOptionDescription, - HelpName = CliCommandStrings.OutputOptionName + Description = CommandDefinitionStrings.BuildOutputOptionDescription, + HelpName = CommandDefinitionStrings.OutputOptionName }.ForwardAsOutputPath("OutputPath"); public readonly Option NoIncrementalOption = new Option("--no-incremental") { - Description = CliCommandStrings.NoIncrementalOptionDescription, + Description = CommandDefinitionStrings.NoIncrementalOptionDescription, Arity = ArgumentArity.Zero }.ForwardAs("--target:Rebuild"); public readonly Option NoDependenciesOption = new Option("--no-dependencies") { - Description = CliCommandStrings.NoDependenciesOptionDescription, + Description = CommandDefinitionStrings.NoDependenciesOptionDescription, Arity = ArgumentArity.Zero }.ForwardAs("--property:BuildProjectReferences=false"); @@ -46,11 +45,11 @@ internal sealed class BuildCommandDefinition : Command public readonly Option NoSelfContainedOption = CommonOptions.CreateNoSelfContainedOption(); - public readonly TargetPlatformOptions TargetPlatformOptions = new(CliCommandStrings.BuildRuntimeOptionDescription); + public readonly TargetPlatformOptions TargetPlatformOptions = new(CommandDefinitionStrings.BuildRuntimeOptionDescription); - public readonly Option FrameworkOption = CommonOptions.CreateFrameworkOption(CliCommandStrings.BuildFrameworkOptionDescription); + public readonly Option FrameworkOption = CommonOptions.CreateFrameworkOption(CommandDefinitionStrings.BuildFrameworkOptionDescription); - public readonly Option ConfigurationOption = CommonOptions.CreateConfigurationOption(CliCommandStrings.BuildConfigurationOptionDescription); + public readonly Option ConfigurationOption = CommonOptions.CreateConfigurationOption(CommandDefinitionStrings.BuildConfigurationOptionDescription); /// /// Build actually means 'run the default Target' generally in MSBuild @@ -69,7 +68,7 @@ internal sealed class BuildCommandDefinition : Command public readonly Option GetResultOutputFileOption = CommonOptions.CreateGetResultOutputFileOption(); public BuildCommandDefinition() - : base("build", CliCommandStrings.BuildAppFullName) + : base("build", CommandDefinitionStrings.BuildAppFullName) { this.DocsLink = Link; diff --git a/src/Cli/dotnet/Commands/BuildServer/BuildServerCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerCommandDefinition.cs similarity index 88% rename from src/Cli/dotnet/Commands/BuildServer/BuildServerCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerCommandDefinition.cs index b01e610a59b3..bce60663fb18 100644 --- a/src/Cli/dotnet/Commands/BuildServer/BuildServerCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerCommandDefinition.cs @@ -14,7 +14,7 @@ internal sealed class BuildServerCommandDefinition : Command public readonly BuildServerShutdownCommandDefinition ShutdownCommand = new(); public BuildServerCommandDefinition() - : base("build-server", CliCommandStrings.BuildServerCommandDescription) + : base("build-server", CommandDefinitionStrings.BuildServerCommandDescription) { this.DocsLink = Link; Subcommands.Add(ShutdownCommand); diff --git a/src/Cli/dotnet/Commands/BuildServer/Shutdown/BuildServerShutdownCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerShutdownCommandDefinition.cs similarity index 72% rename from src/Cli/dotnet/Commands/BuildServer/Shutdown/BuildServerShutdownCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerShutdownCommandDefinition.cs index 91b657d04d01..3d68c52eb9ae 100644 --- a/src/Cli/dotnet/Commands/BuildServer/Shutdown/BuildServerShutdownCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/BuildServer/BuildServerShutdownCommandDefinition.cs @@ -9,24 +9,24 @@ internal sealed class BuildServerShutdownCommandDefinition : Command { public readonly Option MSBuildOption = new("--msbuild") { - Description = CliCommandStrings.MSBuildOptionDescription, + Description = CommandDefinitionStrings.MSBuildOptionDescription, Arity = ArgumentArity.Zero }; public readonly Option VbcsOption = new("--vbcscompiler") { - Description = CliCommandStrings.VBCSCompilerOptionDescription, + Description = CommandDefinitionStrings.VBCSCompilerOptionDescription, Arity = ArgumentArity.Zero }; public readonly Option RazorOption = new("--razor") { - Description = CliCommandStrings.RazorOptionDescription, + Description = CommandDefinitionStrings.RazorOptionDescription, Arity = ArgumentArity.Zero }; public BuildServerShutdownCommandDefinition() - : base("shutdown", CliCommandStrings.BuildServerShutdownCommandDescription) + : base("shutdown", CommandDefinitionStrings.BuildServerShutdownCommandDescription) { Options.Add(MSBuildOption); Options.Add(VbcsOption); diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanCommandDefinition.cs similarity index 82% rename from src/Cli/dotnet/Commands/Clean/CleanCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanCommandDefinition.cs index 272a005d7b30..2e3daf3031cb 100644 --- a/src/Cli/dotnet/Commands/Clean/CleanCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanCommandDefinition.cs @@ -4,7 +4,6 @@ using System.CommandLine; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts; -using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Clean; @@ -13,29 +12,29 @@ internal sealed class CleanCommandDefinition : Command public new const string Name = "clean"; private const string Link = "https://aka.ms/dotnet-clean"; - public readonly Argument SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName) + public readonly Argument SlnOrProjectOrFileArgument = new(CommandDefinitionStrings.SolutionOrProjectOrFileArgumentName) { - Description = CliStrings.SolutionOrProjectOrFileArgumentDescription, + Description = CommandDefinitionStrings.SolutionOrProjectOrFileArgumentDescription, Arity = ArgumentArity.ZeroOrMore }; public readonly Option OutputOption = new Option("--output", "-o") { - Description = CliCommandStrings.CleanCmdOutputDirDescription, - HelpName = CliCommandStrings.CleanCmdOutputDir + Description = CommandDefinitionStrings.CleanCmdOutputDirDescription, + HelpName = CommandDefinitionStrings.CleanCmdOutputDir }.ForwardAsOutputPath("OutputPath"); public readonly Option NoLogoOption = CommonOptions.CreateNoLogoOption(); - public readonly Option FrameworkOption = CommonOptions.CreateFrameworkOption(CliCommandStrings.CleanFrameworkOptionDescription); + public readonly Option FrameworkOption = CommonOptions.CreateFrameworkOption(CommandDefinitionStrings.CleanFrameworkOptionDescription); - public readonly Option ConfigurationOption = CommonOptions.CreateConfigurationOption(CliCommandStrings.CleanConfigurationOptionDescription); + public readonly Option ConfigurationOption = CommonOptions.CreateConfigurationOption(CommandDefinitionStrings.CleanConfigurationOptionDescription); public readonly Option TargetOption = CreateTargetOption(); public readonly Option VerbosityOption = CommonOptions.CreateVerbosityOption(Utils.VerbosityOptions.normal); - public readonly Option RuntimeOption = TargetPlatformOptions.CreateRuntimeOption(CliCommandStrings.CleanRuntimeOptionDescription); + public readonly Option RuntimeOption = TargetPlatformOptions.CreateRuntimeOption(CommandDefinitionStrings.CleanRuntimeOptionDescription); public readonly Option InteractiveOption = CommonOptions.CreateInteractiveMsBuildForwardOption(); public readonly Option ArtifactsPathOption = CommonOptions.CreateArtifactsPathOption(); public readonly Option DisableBuildServersOption = CommonOptions.CreateDisableBuildServersOption(); @@ -47,7 +46,7 @@ internal sealed class CleanCommandDefinition : Command public readonly Command FileBasedAppsCommand = new CleanFileBasedAppArtifactsCommandDefinition(); public CleanCommandDefinition() - : base(Name, CliCommandStrings.CleanAppFullName) + : base(Name, CommandDefinitionStrings.CleanAppFullName) { this.DocsLink = Link; diff --git a/src/Cli/dotnet/Commands/Clean/FileBasedAppArtifacts/CleanFileBasedAppArtifactsCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanFileBasedAppArtifactsCommandDefinition.cs similarity index 77% rename from src/Cli/dotnet/Commands/Clean/FileBasedAppArtifacts/CleanFileBasedAppArtifactsCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanFileBasedAppArtifactsCommandDefinition.cs index 9071e3cdcc63..4d44e80538fb 100644 --- a/src/Cli/dotnet/Commands/Clean/FileBasedAppArtifacts/CleanFileBasedAppArtifactsCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Clean/CleanFileBasedAppArtifactsCommandDefinition.cs @@ -11,13 +11,13 @@ internal sealed class CleanFileBasedAppArtifactsCommandDefinition : Command public readonly Option DryRunOption = new("--dry-run") { - Description = CliCommandStrings.CleanFileBasedAppArtifactsDryRun, + Description = CommandDefinitionStrings.CleanFileBasedAppArtifactsDryRun, Arity = ArgumentArity.Zero, }; public readonly Option DaysOption = new("--days") { - Description = CliCommandStrings.CleanFileBasedAppArtifactsDays, + Description = CommandDefinitionStrings.CleanFileBasedAppArtifactsDays, DefaultValueFactory = _ => 30, }; @@ -25,7 +25,7 @@ internal sealed class CleanFileBasedAppArtifactsCommandDefinition : Command /// /// Specified internally when the command is started automatically in background by dotnet run. - /// Causes to be updated. + /// Causes RunFileArtifactsMetadata.LastAutomaticCleanupUtc to be updated. /// public readonly Option AutomaticOption = new(AutomaticOptionName) { @@ -33,7 +33,7 @@ internal sealed class CleanFileBasedAppArtifactsCommandDefinition : Command }; public CleanFileBasedAppArtifactsCommandDefinition() - : base(Name, CliCommandStrings.CleanFileBasedAppArtifactsCommandDescription) + : base(Name, CommandDefinitionStrings.CleanFileBasedAppArtifactsCommandDescription) { Hidden = true; Options.Add(DryRunOption); diff --git a/src/Cli/dotnet/Commands/Dnx/DnxCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Dnx/DnxCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Dnx/DnxCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Dnx/DnxCommandDefinition.cs diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs new file mode 100644 index 000000000000..a7357a9f75bc --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.CommandLine.Completions; +using System.CommandLine.StaticCompletions; +using Microsoft.DotNet.Cli.Commands.Build; +using Microsoft.DotNet.Cli.Commands.BuildServer; +using Microsoft.DotNet.Cli.Commands.Clean; +using Microsoft.DotNet.Cli.Commands.Dnx; +using Microsoft.DotNet.Cli.Commands.Format; +using Microsoft.DotNet.Cli.Commands.Fsi; +using Microsoft.DotNet.Cli.Commands.Help; +using Microsoft.DotNet.Cli.Commands.Hidden.Add; +using Microsoft.DotNet.Cli.Commands.Hidden.Complete; +using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess; +using Microsoft.DotNet.Cli.Commands.Hidden.List; +using Microsoft.DotNet.Cli.Commands.Hidden.Parse; +using Microsoft.DotNet.Cli.Commands.Hidden.Remove; +using Microsoft.DotNet.Cli.Commands.MSBuild; +using Microsoft.DotNet.Cli.Commands.New; +using Microsoft.DotNet.Cli.Commands.NuGet; +using Microsoft.DotNet.Cli.Commands.Pack; +using Microsoft.DotNet.Cli.Commands.Package; +using Microsoft.DotNet.Cli.Commands.Project; +using Microsoft.DotNet.Cli.Commands.Publish; +using Microsoft.DotNet.Cli.Commands.Reference; +using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Commands.Run.Api; +using Microsoft.DotNet.Cli.Commands.Sdk; +using Microsoft.DotNet.Cli.Commands.Solution; +using Microsoft.DotNet.Cli.Commands.Test; +using Microsoft.DotNet.Cli.Commands.Tool; +using Microsoft.DotNet.Cli.Commands.Tool.Store; +using Microsoft.DotNet.Cli.Commands.VSTest; +using Microsoft.DotNet.Cli.Commands.Workload; + +namespace Microsoft.DotNet.Cli.Commands; + +internal sealed class DotNetCommandDefinition : RootCommand +{ + public readonly Argument DotnetSubCommand = new("subcommand") + { + Arity = ArgumentArity.ZeroOrOne, + Hidden = true + }; + + public readonly Option DiagOption = CommonOptions.CreateDiagnosticsOption(recursive: false); + + public readonly Option VersionOption = new("--version") + { + Arity = ArgumentArity.Zero + }; + + public readonly Option InfoOption = new("--info") + { + Arity = ArgumentArity.Zero + }; + + public readonly Option ListSdksOption = new("--list-sdks") + { + Arity = ArgumentArity.Zero + }; + + public readonly Option ListRuntimesOption = new("--list-runtimes") + { + Arity = ArgumentArity.Zero + }; + + public readonly Option CliSchemaOption = new("--cli-schema") + { + Description = CommandDefinitionStrings.SDKSchemaCommandDefinition, + Arity = ArgumentArity.Zero, + Recursive = true, + Hidden = true, + }; + + public readonly AddCommandDefinition AddCommand; + public readonly BuildCommandDefinition BuildCommand; + public readonly BuildServerCommandDefinition BuildServerCommand; + public readonly CleanCommandDefinition CleanCommand; + public readonly DnxCommandDefinition DnxCommand; + public readonly FormatCommandDefinition FormatCommand; + public readonly CompleteCommandDefinition CompleteCommand; + public readonly FsiCommandDefinition FsiCommand; + public readonly ListCommandDefinition ListCommand; + public readonly MSBuildCommandDefinition MSBuildCommand; + public readonly NewCommandDefinition NewCommand; + public readonly NuGetCommandDefinition NuGetCommand; + public readonly PackCommandDefinition PackCommand; + public readonly PackageCommandDefinition PackageCommand; + public readonly ParseCommandDefinition ParseCommand; + public readonly ProjectCommandDefinition ProjectCommand; + public readonly PublishCommandDefinition PublishCommand; + public readonly ReferenceCommandDefinition ReferenceCommand; + public readonly RemoveCommandDefinition RemoveCommand; + public readonly RestoreCommandDefinition RestoreCommand; + public readonly RunCommandDefinition RunCommand; + public readonly RunApiCommandDefinition RunApiCommand; + public readonly SolutionCommandDefinition SolutionCommand; + public readonly StoreCommandDefinition StoreCommand; + public readonly ToolCommandDefinition ToolCommand; + public readonly VSTestCommandDefinition VSTestCommand; + public readonly HelpCommandDefinition HelpCommand; + public readonly SdkCommandDefinition SdkCommand; + public readonly InternalReportInstallSuccessCommandDefinition InternalReportInstallSuccessCommand; + public readonly WorkloadCommandDefinition WorkloadCommand; + public readonly TestCommandDefinition TestCommand; + public readonly CompletionsCommandDefinition CompletionsCommand; + + public DotNetCommandDefinition() + : base("dotnet") + { + Directives.Add(new DiagramDirective()); + Directives.Add(new SuggestDirective()); + Directives.Add(new EnvironmentVariablesDirective()); + + Arguments.Add(DotnetSubCommand); + + Options.Add(DiagOption); + Options.Add(VersionOption); + Options.Add(InfoOption); + Options.Add(ListSdksOption); + Options.Add(ListRuntimesOption); + Options.Add(CliSchemaOption); + + Subcommands.Add(AddCommand = new()); + Subcommands.Add(BuildCommand = new()); + Subcommands.Add(BuildServerCommand = new()); + Subcommands.Add(CleanCommand = new()); + Subcommands.Add(DnxCommand = new()); + Subcommands.Add(FormatCommand = new()); + Subcommands.Add(CompleteCommand = new()); + Subcommands.Add(FsiCommand = new()); + Subcommands.Add(ListCommand = new()); + Subcommands.Add(MSBuildCommand = new()); + Subcommands.Add(NewCommand = new()); + Subcommands.Add(NuGetCommand = new()); + Subcommands.Add(PackCommand = new()); + Subcommands.Add(PackageCommand = new()); + Subcommands.Add(ParseCommand = new()); + Subcommands.Add(ProjectCommand = new()); + Subcommands.Add(PublishCommand = new()); + Subcommands.Add(ReferenceCommand = new()); + Subcommands.Add(RemoveCommand = new()); + Subcommands.Add(RestoreCommand = new()); + Subcommands.Add(RunCommand = new()); + Subcommands.Add(RunApiCommand = new()); + Subcommands.Add(SolutionCommand = new()); + Subcommands.Add(StoreCommand = new()); + Subcommands.Add(TestCommand = TestCommandDefinition.Create()); + Subcommands.Add(ToolCommand = new()); + Subcommands.Add(VSTestCommand = new()); + Subcommands.Add(HelpCommand = new()); + Subcommands.Add(SdkCommand = new()); + Subcommands.Add(InternalReportInstallSuccessCommand = new()); + Subcommands.Add(WorkloadCommand = new()); + Subcommands.Add(CompletionsCommand = new()); + } +} diff --git a/src/Cli/dotnet/Commands/Format/FormatCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Format/FormatCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Format/FormatCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Format/FormatCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Fsi/FsiCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Fsi/FsiCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Fsi/FsiCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Fsi/FsiCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Help/HelpCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Help/HelpCommandDefinition.cs similarity index 69% rename from src/Cli/dotnet/Commands/Help/HelpCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Help/HelpCommandDefinition.cs index 183398efdad1..ceafa6a37ee2 100644 --- a/src/Cli/dotnet/Commands/Help/HelpCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Help/HelpCommandDefinition.cs @@ -10,14 +10,14 @@ internal sealed class HelpCommandDefinition : Command { private const string Link = "https://aka.ms/dotnet-help"; - public readonly Argument Argument = new(CliCommandStrings.CommandArgumentName) + public readonly Argument Argument = new(CommandDefinitionStrings.CommandArgumentName) { - Description = CliCommandStrings.CommandArgumentDescription, + Description = CommandDefinitionStrings.CommandArgumentDescription, Arity = ArgumentArity.ZeroOrMore }; public HelpCommandDefinition() - : base("help", CliCommandStrings.HelpAppFullName) + : base("help", CommandDefinitionStrings.HelpAppFullName) { this.DocsLink = Link; Arguments.Add(Argument); diff --git a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddCommandDefinition.cs similarity index 94% rename from src/Cli/dotnet/Commands/Hidden/Add/AddCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddCommandDefinition.cs index f7ab0583658e..632a4ee0ac38 100644 --- a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddCommandDefinition.cs @@ -20,7 +20,7 @@ internal sealed class AddCommandDefinition : Command public readonly Argument ProjectOrFileArgument = PackageCommandDefinition.CreateProjectOrFileArgument(); public AddCommandDefinition() - : base(Name, CliCommandStrings.NetAddCommand) + : base(Name, CommandDefinitionStrings.NetAddCommand) { Hidden = true; this.DocsLink = Link; diff --git a/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddPackageCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddPackageCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/Add/Reference/AddReferenceCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddReferenceCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Add/Reference/AddReferenceCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Add/AddReferenceCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Complete/CompleteCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Complete/CompleteCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/List/ListCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs similarity index 85% rename from src/Cli/dotnet/Commands/Hidden/List/ListCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs index c461e6371f00..ec63176267cd 100644 --- a/src/Cli/dotnet/Commands/Hidden/List/ListCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs @@ -20,13 +20,13 @@ public static Argument CreateSlnOrProjectArgument(string name, string de Arity = ArgumentArity.ZeroOrOne }.DefaultToCurrentDirectory(); - public readonly Argument SlnOrProjectArgument = CreateSlnOrProjectArgument(CliStrings.SolutionOrProjectArgumentName, CliStrings.SolutionOrProjectArgumentDescription); + public readonly Argument SlnOrProjectArgument = CreateSlnOrProjectArgument(CommandDefinitionStrings.SolutionOrProjectArgumentName, CommandDefinitionStrings.SolutionOrProjectArgumentDescription); public readonly ListPackageCommandDefinition PackageCommand = new(); public readonly ListReferenceCommandDefinition ReferenceCommand = new(); public ListCommandDefinition() - : base(Name, CliCommandStrings.NetListCommand) + : base(Name, CommandDefinitionStrings.NetListCommand) { Hidden = true; this.DocsLink = Link; diff --git a/src/Cli/dotnet/Commands/Hidden/List/Package/ListPackageCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/List/Package/ListPackageCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/List/Reference/ListReferenceCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs similarity index 93% rename from src/Cli/dotnet/Commands/Hidden/List/Reference/ListReferenceCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs index c3d48abc98d0..748187d7f4fe 100644 --- a/src/Cli/dotnet/Commands/Hidden/List/Reference/ListReferenceCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs @@ -25,7 +25,7 @@ public ListReferenceCommandDefinition() : base(Name) internal abstract class ListReferenceCommandDefinitionBase : Command { public ListReferenceCommandDefinitionBase(string name) - : base(name, CliCommandStrings.ReferenceListAppFullName) + : base(name, CommandDefinitionStrings.ReferenceListAppFullName) { } diff --git a/src/Cli/dotnet/Commands/Hidden/Parse/ParseCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Parse/ParseCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Parse/ParseCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Parse/ParseCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemoveCommandDefinition.cs similarity index 94% rename from src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemoveCommandDefinition.cs index e10fddd7fe01..ca286e026895 100644 --- a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemoveCommandDefinition.cs @@ -21,7 +21,7 @@ internal sealed class RemoveCommandDefinition : Command public readonly RemoveReferenceCommandDefinition ReferenceCommand = new(); public RemoveCommandDefinition() - : base(Name, CliCommandStrings.NetRemoveCommand) + : base(Name, CommandDefinitionStrings.NetRemoveCommand) { Hidden = true; this.DocsLink = Link; diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemovePackageCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemovePackageCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/Reference/RemoveReferenceCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemoveReferenceCommandDefinition.cs similarity index 100% rename from src/Cli/dotnet/Commands/Hidden/Remove/Reference/RemoveReferenceCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/Remove/RemoveReferenceCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/MSBuild/MSBuildCommandDefinition.cs similarity index 92% rename from src/Cli/dotnet/Commands/MSBuild/MSBuildCommandDefinition.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/MSBuild/MSBuildCommandDefinition.cs index c1bc0a00cb50..1a0bbc3c12e3 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/MSBuild/MSBuildCommandDefinition.cs @@ -15,7 +15,7 @@ internal sealed class MSBuildCommandDefinition : Command public readonly Option DisableBuildServersOption = CommonOptions.CreateDisableBuildServersOption(); public MSBuildCommandDefinition() - : base("msbuild", CliCommandStrings.BuildAppFullName) + : base("msbuild", CommandDefinitionStrings.BuildAppFullName) { this.DocsLink = Link; base.Arguments.Add(Arguments); diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/CommandDefinitions/CommandDefinitionExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/New/CommandDefinitionExtensions.cs similarity index 61% rename from src/Cli/Microsoft.TemplateEngine.Cli/CommandDefinitions/CommandDefinitionExtensions.cs rename to src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/New/CommandDefinitionExtensions.cs index 031003bef7d9..46111935cafa 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/CommandDefinitions/CommandDefinitionExtensions.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/New/CommandDefinitionExtensions.cs @@ -1,23 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable SA1202 // Elements should be ordered by access -#pragma warning disable SA1308 // Variable names should not be prefixed -#pragma warning disable SA1311 // Static readonly fields should begin with upper-case letter -#pragma warning disable CA1810 // Initialize reference type static fields inline -#pragma warning disable SA1010 // Opening square brackets should be spaced correctly - using System.CommandLine; using System.CommandLine.Parsing; -namespace Microsoft.TemplateEngine.Cli; +namespace Microsoft.DotNet.Cli.Commands.New; internal static class CommandDefinitionExtensions { - public static TDefinition AddShortNameArgumentValidator(this TDefinition command, Argument nameArgument) - where TDefinition : CommandDefinition + public static TDefinition AddShortNameArgumentValidator(this TDefinition definition, Argument nameArgument) + where TDefinition : Command { - command.Validators.Add(commandResult => + definition.Validators.Add(commandResult => { var nameArgumentResult = commandResult.Children.FirstOrDefault(symbol => symbol is ArgumentResult argumentResult && argumentResult.Argument == nameArgument); if (nameArgumentResult == null) @@ -25,28 +19,28 @@ public static TDefinition AddShortNameArgumentValidator(this TDefin return; } - ValidateArgumentUsage(commandResult, CommandDefinition.New.ShortNameArgument); + ValidateArgumentUsage(commandResult, NewCommandDefinition.ShortNameArgumentName); }); - return command; + return definition; } - public static TDefinition AddNoLegacyUsageValidators(this TDefinition command, params IEnumerable except) - where TDefinition : CommandDefinition + public static TDefinition AddNoLegacyUsageValidators(this TDefinition command, params IEnumerable except) + where TDefinition : Command { - foreach (var option in CommandDefinition.New.LegacyOptions) + foreach (var optionName in LegacyOptions.AllNames) { - if (!except.Contains(option)) + if (!except.Contains(optionName)) { - command.Validators.Add(symbolResult => symbolResult.ValidateOptionUsage(option)); + command.Validators.Add(symbolResult => symbolResult.ValidateOptionUsage(optionName)); } } - foreach (var argument in new Argument[] { CommandDefinition.New.ShortNameArgument, CommandDefinition.New.RemainingArguments }) + foreach (var argumentName in new[] { NewCommandDefinition.ShortNameArgumentName, NewCommandDefinition.RemainingArgumentsName }) { - if (!except.Contains(argument)) + if (!except.Contains(argumentName)) { - command.Validators.Add(symbolResult => symbolResult.ValidateArgumentUsage(argument)); + command.Validators.Add(symbolResult => symbolResult.ValidateArgumentUsage(argumentName)); } } @@ -54,13 +48,13 @@ public static TDefinition AddNoLegacyUsageValidators(this TDefiniti } public static TDefinition AddOptions(this TDefinition command, IEnumerable