diff --git a/Directory.Packages.props b/Directory.Packages.props index 104dedfd9eaf..e10167b6516c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,6 +44,10 @@ + + + + diff --git a/documentation/specs/unified-configuration.md b/documentation/specs/unified-configuration.md new file mode 100644 index 000000000000..3f1ab3fc0ad0 --- /dev/null +++ b/documentation/specs/unified-configuration.md @@ -0,0 +1,1061 @@ +# Unified Configuration System for .NET CLI + +## Overview + +This specification outlines the plan to refactor the .NET CLI codebase to replace direct usage of global.json and environment variables with a unified configuration system based on Microsoft.Extensions.Configuration.IConfiguration. + +## Goals + +- Replace direct `Environment.GetEnvironmentVariable()` calls with a unified configuration system +- Replace direct `global.json` file reading with configuration providers +- Establish a clear configuration hierarchy and precedence +- Maintain backward compatibility with existing environment variables and global.json usage +- Provide a foundation for future configuration enhancements (e.g., dotnet.config support) + +## Current State Analysis + +The codebase currently has: +1. Direct `Environment.GetEnvironmentVariable()` calls throughout various classes +2. An `EnvironmentProvider` abstraction that wraps environment variable access +3. Custom `global.json` reading in several places (e.g., `GlobalJsonWorkloadSetsFile.cs`, `RuntimeConfig.cs`) +4. Some use of Microsoft.Extensions.Configuration in test infrastructure but not in the main CLI + +## Configuration Hierarchy + +The new unified configuration system will follow this precedence order (highest to lowest priority): + +1. **Command-line arguments** (handled separately by existing System.CommandLine infrastructure) +2. **Environment variables with DOTNET_ prefix** (e.g., `DOTNET_CLI_TELEMETRY_OPTOUT`) +3. **global.json** (custom configuration provider) +4. **dotnet.config** (future enhancement - INI configuration file) + +**Note:** System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface as they are not specific to .NET CLI configuration. + +## Implementation Plan + +### Phase 1: Infrastructure + +#### 1.1 Core Configuration Builder with Strongly-Typed Configuration + +Create a centralized configuration builder in the Microsoft.Extensions.Configuration.DotnetCli project: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +public class DotNetConfiguration +{ + public static IConfiguration Create(string workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Priority order (last wins): + // 1. dotnet.config (if it exists) - with section-based key mapping + // 2. global.json (custom provider with key mapping) + // 3. Environment variables with DOTNET_ prefix (with key mapping) + // 4. Command line arguments (handled separately) + + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Add dotnet.config if it exists (future enhancement) + var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config"); + if (File.Exists(dotnetConfigPath)) + { + builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false); + } + + // Add global.json with a custom configuration provider that maps keys + builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // Add DOTNET_ prefixed environment variables with key mapping + builder.Add(new DotNetEnvironmentConfigurationSource()); + + return builder.Build(); + } + + public static DotNetConfigurationRoot CreateTyped(string workingDirectory = null) + { + var configuration = Create(workingDirectory); + return new DotNetConfigurationRoot(configuration); + } + + // Lightweight factory for scenarios that only need basic configuration access + public static DotNetConfigurationRoot CreateMinimal(string workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Only add environment variables for minimal overhead + builder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = builder.Build(); + return new DotNetConfigurationRoot(configuration); + } +} +``` + +**Performance Considerations:** +- **Lazy Initialization**: All strongly-typed configuration properties use `Lazy` to defer expensive binding operations until first access +- **Minimal Factory**: `CreateMinimal()` provides a lightweight option that only loads environment variables +- **Provider Ordering**: Most expensive providers (global.json file I/O) are added last to minimize impact when not needed + +#### 1.2 Enhanced Global.json Configuration Provider + +Create a custom configuration provider for global.json files with key mapping: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +public class GlobalJsonConfigurationProvider : ConfigurationProvider +{ + private readonly string _path; + + private static readonly Dictionary GlobalJsonKeyMappings = new() + { + ["sdk:version"] = "sdk:version", + ["sdk:rollForward"] = "sdk:rollforward", + ["sdk:allowPrerelease"] = "sdk:allowprerelease", + ["msbuild-sdks"] = "msbuild:sdks", + // Add more mappings as the global.json schema evolves + }; + + public GlobalJsonConfigurationProvider(string workingDirectory) + { + _path = FindGlobalJson(workingDirectory); + } + + public override void Load() + { + Data.Clear(); + + if (_path == null || !File.Exists(_path)) + return; + + try + { + var json = File.ReadAllText(_path); + var document = JsonDocument.Parse(json); + + LoadGlobalJsonData(document.RootElement, ""); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error parsing global.json at {_path}", ex); + } + } + + private void LoadGlobalJsonData(JsonElement element, string prefix) + { + foreach (var property in element.EnumerateObject()) + { + var rawKey = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}:{property.Name}"; + + switch (property.Value.ValueKind) + { + case JsonValueKind.Object: + LoadGlobalJsonData(property.Value, rawKey); + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + // Map to canonical key format + var canonicalKey = MapGlobalJsonKey(rawKey); + Data[canonicalKey] = GetValueAsString(property.Value); + break; + } + } + } + + private string MapGlobalJsonKey(string rawKey) + { + // Check for exact mapping first + if (GlobalJsonKeyMappings.TryGetValue(rawKey, out var mapped)) + return mapped; + + // For msbuild-sdks, convert to msbuild:sdks:packagename format + if (rawKey.StartsWith("msbuild-sdks:")) + return rawKey.Replace("msbuild-sdks:", "msbuild:sdks:"); + + // Default: convert to lowercase and normalize separators + return rawKey.ToLowerInvariant().Replace("-", ":"); + } + + private string GetValueAsString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + _ => element.GetRawText() + }; + } + + private string FindGlobalJson(string startDirectory) + { + var current = new DirectoryInfo(startDirectory); + while (current != null) + { + var globalJsonPath = Path.Combine(current.FullName, "global.json"); + if (File.Exists(globalJsonPath)) + return globalJsonPath; + current = current.Parent; + } + return null; + } +} +``` + +#### 1.3 Environment Variable Configuration Provider with Key Mapping + +Create a custom environment variable provider that maps DOTNET_ variables to canonical keys: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider +{ + private static readonly Dictionary EnvironmentKeyMappings = new() + { + ["DOTNET_HOST_PATH"] = "dotnet:host:path", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "dotnet:cli:telemetry:optout", + ["DOTNET_NOLOGO"] = "dotnet:cli:nologo", + ["DOTNET_CLI_PERF_LOG"] = "dotnet:cli:perf:log", + ["DOTNET_MULTILEVEL_LOOKUP"] = "dotnet:host:multilevel:lookup", + ["DOTNET_ROLL_FORWARD"] = "dotnet:runtime:rollforward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "dotnet:runtime:rollforward:onnocandidate", + ["DOTNET_CLI_HOME"] = "dotnet:cli:home", + ["DOTNET_CLI_CONTEXT_VERBOSE"] = "dotnet:cli:context:verbose", + ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "dotnet:cli:firsttime:skip", + ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "dotnet:tools:addtopath", + // Add more mappings as needed + }; + + public override void Load() + { + Data.Clear(); + + foreach (var mapping in EnvironmentKeyMappings) + { + var value = Environment.GetEnvironmentVariable(mapping.Key); + if (!string.IsNullOrEmpty(value)) + { + Data[mapping.Value] = value; + } + } + } +} + +public class DotNetEnvironmentConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new DotNetEnvironmentConfigurationProvider(); + } +} +``` + +#### 1.4 Strongly-Typed Configuration Root with Lazy Initialization + +Create a strongly-typed configuration service that uses lazy initialization and the configuration binding source generator: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationRoot.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +public class DotNetConfigurationRoot +{ + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + + public DotNetConfigurationRoot(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + GetConfigurationSection("CliUserExperience") ?? new()); + _runtimeHost = new Lazy(() => + GetConfigurationSection("RuntimeHost") ?? new()); + _build = new Lazy(() => + GetConfigurationSection("Build") ?? new()); + _sdkResolver = new Lazy(() => + GetConfigurationSection("SdkResolver") ?? new()); + _workload = new Lazy(() => + GetConfigurationSection("Workload") ?? new()); + _firstTimeUse = new Lazy(() => + GetConfigurationSection("FirstTimeUse") ?? new()); + _development = new Lazy(() => + GetConfigurationSection("Development") ?? new()); + _tool = new Lazy(() => + GetConfigurationSection("Tool") ?? new()); + } + + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + + // Generic value access for backward compatibility + public string? GetValue(string key, string? defaultValue = null) => _configuration[key] ?? defaultValue; + public T GetValue(string key, T defaultValue = default) => _configuration.GetValue(key, defaultValue); + + private T? GetConfigurationSection(string sectionName) where T : class, new() + { + var section = _configuration.GetSection(sectionName); + // Uses configuration binding source generator for AOT-friendly binding + return section.Exists() ? section.Get() : null; + } +} +``` + + public T GetValue(string canonicalKey, T defaultValue = default) + { + return Configuration.GetValue(canonicalKey, defaultValue); + } + + public bool GetBoolValue(string canonicalKey, bool defaultValue = false) + { + var value = Configuration[canonicalKey]; + if (string.IsNullOrEmpty(value)) + return defaultValue; + + return value.ToLowerInvariant() switch + { + "true" or "1" or "yes" => true, + "false" or "0" or "no" => false, + _ => defaultValue + }; + } + + // Helper methods for common configuration values + public bool IsTelemetryOptOut() => GetBoolValue("dotnet:cli:telemetry:optout", false); + public bool IsNoLogo() => GetBoolValue("dotnet:cli:nologo", false); + public string GetHostPath() => GetValue("dotnet:host:path"); + public string GetSdkVersion() => GetValue("sdk:version"); +} +``` + +### Phase 2: Integration + +#### 2.1 Update Program.cs + +Update the main entry point to initialize configuration early with canonical key access: + +```csharp +public class Program +{ + public static IConfiguration GlobalConfiguration { get; private set; } + public static IConfigurationService ConfigurationService { get; private set; } + + public static int Main(string[] args) + { + // Initialize configuration early + GlobalConfiguration = DotNetConfiguration.Create(); + ConfigurationService = new ConfigurationService(GlobalConfiguration); + + // Replace direct env var calls with configuration using canonical keys + bool perfLogEnabled = ConfigurationService.GetBoolValue("dotnet:cli:perf:log", false); + bool noLogo = ConfigurationService.IsNoLogo(); + + // Continue with existing logic... + } +} +``` + +#### 2.2 Configuration-Based Environment Provider with Key Mapping + +Create a bridge between the new configuration system and existing `IEnvironmentProvider` interface: + +```csharp +public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider +{ + private readonly IConfigurationService _configurationService; + private readonly IEnvironmentProvider _fallbackProvider; + + // Reverse mapping from environment variable names to canonical keys + private static readonly Dictionary EnvironmentToCanonicalMappings = new() + { + ["DOTNET_HOST_PATH"] = "dotnet:host:path", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "dotnet:cli:telemetry:optout", + ["DOTNET_NOLOGO"] = "dotnet:cli:nologo", + ["DOTNET_CLI_PERF_LOG"] = "dotnet:cli:perf:log", + ["DOTNET_MULTILEVEL_LOOKUP"] = "dotnet:host:multilevel:lookup", + ["DOTNET_ROLL_FORWARD"] = "dotnet:runtime:rollforward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "dotnet:runtime:rollforward:onnocandidate", + // Add more mappings as needed + }; + + public ConfigurationBasedEnvironmentProvider( + IConfigurationService configurationService, + IEnvironmentProvider fallbackProvider = null) + { + _configurationService = configurationService; + _fallbackProvider = fallbackProvider ?? new EnvironmentProvider(); + } + + public string GetEnvironmentVariable(string name) + { + // For DOTNET_ prefixed variables, try configuration service first using canonical key + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase) && + EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) + { + var configValue = _configurationService.GetValue(canonicalKey); + if (configValue != null) + return configValue; + } + + // For all other variables or if not found in config, use fallback provider + return _fallbackProvider.GetEnvironmentVariable(name); + } + + public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) + { + // For DOTNET_ prefixed variables, use configuration service with canonical key + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase) && + EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) + { + return _configurationService.GetBoolValue(canonicalKey, defaultValue); + } + + // For all other variables, use fallback provider + return _fallbackProvider.GetEnvironmentVariableAsBool(name, defaultValue); + } + + // Implement other members as pass-through to fallback provider + public IEnumerable ExecutableExtensions => _fallbackProvider.ExecutableExtensions; + + public string GetCommandPath(string commandName, params string[] extensions) + => _fallbackProvider.GetCommandPath(commandName, extensions); + + public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + + public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + + public string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) + => _fallbackProvider.GetEnvironmentVariable(variable, target); + + public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) + => _fallbackProvider.SetEnvironmentVariable(variable, value, target); +} +``` + +### Phase 3: Migration + +#### 3.1 Systematic Replacement + +Replace direct environment variable access patterns using canonical keys: + +**Before:** +```csharp +var value = Environment.GetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT"); +bool optOut = !string.IsNullOrEmpty(value) && + (value.Equals("true", StringComparison.OrdinalIgnoreCase) || + value.Equals("1")); +``` + +**After (using canonical key):** +```csharp +bool optOut = ConfigurationService.GetBoolValue("dotnet:cli:telemetry:optout", false); +// Or using the helper method: +bool optOut = ConfigurationService.IsTelemetryOptOut(); +``` + +**Before:** +```csharp +var hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); +``` + +**After (using canonical key):** +```csharp +var hostPath = ConfigurationService.GetValue("dotnet:host:path"); +// Or using the helper method: +var hostPath = ConfigurationService.GetHostPath(); +``` + +#### 3.2 Update Direct global.json Readers + +Classes that currently read global.json directly should be updated to use the configuration system: + +**Files to update:** +- `src/Cli/dotnet/Commands/Workload/GlobalJsonWorkloadSetFile.cs` +- `src/Cli/dotnet/RuntimeConfig.cs` +- Any other direct global.json readers found during implementation + +#### 3.3 Update Environment Provider Usages + +Update all instances where `IEnvironmentProvider` is used to use the new `ConfigurationBasedEnvironmentProvider`. + +### Phase 4: Testing & Validation + +#### 4.1 Backward Compatibility Testing + +Ensure all existing functionality continues to work: +- All DOTNET_ prefixed environment variables continue to be respected +- System environment variables (PATH, HOME, etc.) continue to work through the fallback provider +- global.json files are read correctly +- Precedence rules work as expected + +#### 4.2 Unit Tests + +Create comprehensive unit tests for: +- `DotNetConfiguration` class +- `GlobalJsonConfigurationProvider` +- `ConfigurationService` +- `ConfigurationBasedEnvironmentProvider` + +#### 4.3 Integration Tests + +Add integration tests to verify: +- Configuration hierarchy works correctly for DOTNET_ prefixed variables +- System environment variables continue to work through the fallback provider +- global.json files in various directory structures are found +- Environment variable override behavior works correctly + +## Package Dependencies + +The following NuGet package references will need to be added to relevant projects: + +- `Microsoft.Extensions.Configuration` +- `Microsoft.Extensions.Configuration.Abstractions` +- `Microsoft.Extensions.Configuration.EnvironmentVariables` +- `Microsoft.Extensions.Configuration.Json` +- `Microsoft.Extensions.Configuration.Ini` +- `Microsoft.Extensions.Configuration.Binder` + +### Microsoft.Extensions.Configuration.DotnetCli Project + +Create a new project `src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj`: + +```xml + + + + net8.0 + 12.0 + enable + true + false + + + + + + + + + + + +``` + +**Key Features:** +- **Configuration Binding Source Generator**: `EnableConfigurationBindingGenerator=true` enables AOT and trim-friendly configuration binding +- **Strongly-typed Configuration**: Uses `ConfigurationBinder.Get()` for type-safe configuration access +- **.NET 8+ Features**: Takes advantage of C# 12 interceptors for source generation + +**Note:** The configuration binding source generator provides Native AOT and trim-friendly configuration binding without reflection. This is essential for performance and compatibility with .NET's trimming and AOT compilation features. + +### Project Structure + +The Microsoft.Extensions.Configuration.DotnetCli project will be organized as follows: + +``` +src/Microsoft.Extensions.Configuration.DotnetCli/ +├── Microsoft.Extensions.Configuration.DotnetCli.csproj +├── Models/ +│ ├── CliUserExperienceConfiguration.cs +│ ├── RuntimeHostConfiguration.cs +│ ├── BuildConfiguration.cs +│ ├── SdkResolverConfiguration.cs +│ ├── WorkloadConfiguration.cs +│ ├── FirstTimeUseConfiguration.cs +│ ├── DevelopmentConfiguration.cs +│ └── ToolConfiguration.cs +├── Providers/ +│ ├── GlobalJsonConfigurationProvider.cs +│ ├── GlobalJsonConfigurationSource.cs +│ ├── DotNetEnvironmentConfigurationProvider.cs +│ └── DotNetEnvironmentConfigurationSource.cs +└── Services/ + ├── DotNetConfiguration.cs + ├── DotNetConfigurationRoot.cs + ├── DotNetConfigurationService.cs + └── DotNetConfigurationService.cs +``` + +This structure separates concerns into logical groupings: +- **Models/**: Strongly-typed configuration model classes optimized for source generator +- **Providers/**: Custom configuration providers for global.json and DOTNET_ environment variables +- **Services/**: Main configuration services and builders for consumer applications + +## Key Mapping Reference + +### Environment Variables → Canonical Keys +- `DOTNET_CLI_TELEMETRY_OPTOUT` → `dotnet:cli:telemetry:optout` +- `DOTNET_NOLOGO` → `dotnet:cli:nologo` +- `DOTNET_HOST_PATH` → `dotnet:host:path` +- `DOTNET_CLI_PERF_LOG` → `dotnet:cli:perf:log` +- `DOTNET_MULTILEVEL_LOOKUP` → `dotnet:host:multilevel:lookup` +- `DOTNET_ROLL_FORWARD` → `dotnet:runtime:rollforward` + +### Global.json → Canonical Keys +```json +{ + "sdk": { + "version": "6.0.100" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "1.0.0" + } +} +``` + +Maps to canonical keys: +- `sdk:version` → `"6.0.100"` +- `msbuild:sdks:Microsoft.Build.Traversal` → `"1.0.0"` + +### INI Configuration → Canonical Keys +```ini +[dotnet.cli] +telemetryOptOut=true +nologo=true + +[sdk] +version=6.0.100 +``` + +Maps to canonical keys: +- `dotnet:cli:telemetry:optout` → `"true"` +- `dotnet:cli:nologo` → `"true"` +- `sdk:version` → `"6.0.100"` + +**Note:** System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`, `USER`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface and will not be part of the unified configuration system. + +## Typed Configuration Models + +### Configuration Binding Source Generator Compatibility + +All configuration model classes are designed to work with the configuration binding source generator, providing AOT and trim-friendly configuration binding through `ConfigurationBinder.Get()` calls. + +### Functional Configuration Groupings + +Based on analysis of the existing codebase, the following functional groupings have been identified for typed configuration models: + +#### 1. **CLI User Experience Configuration** +Settings that control the CLI's user interface and interaction behavior: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class CliUserExperienceConfiguration +{ + public bool TelemetryOptOut { get; set; } = false; + public bool NoLogo { get; set; } = false; + public bool ForceUtf8Encoding { get; set; } = false; + public string? UILanguage { get; set; } + public string? TelemetryProfile { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_TELEMETRY_OPTOUT` → `TelemetryOptOut` +- `DOTNET_NOLOGO` → `NoLogo` +- `DOTNET_CLI_FORCE_UTF8_ENCODING` → `ForceUtf8Encoding` +- `DOTNET_CLI_UI_LANGUAGE` → `UILanguage` +- `DOTNET_CLI_TELEMETRY_PROFILE` → `TelemetryProfile` + +#### 2. **Runtime Host Configuration** +Settings that control .NET runtime host behavior: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class RuntimeHostConfiguration +{ + public string? HostPath { get; set; } + public bool MultilevelLookup { get; set; } = true; + public string? RollForward { get; set; } + public string? RollForwardOnNoCandidateFx { get; set; } + public string? Root { get; set; } + public string? RootX86 { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_HOST_PATH` → `HostPath` +- `DOTNET_MULTILEVEL_LOOKUP` → `MultilevelLookup` +- `DOTNET_ROLL_FORWARD` → `RollForward` +- `DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX` → `RollForwardOnNoCandidateFx` +- `DOTNET_ROOT` → `Root` +- `DOTNET_ROOT(x86)` → `RootX86` + +#### 3. **Build and MSBuild Configuration** +Settings that control build system behavior: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class BuildConfiguration +{ + public bool RunMSBuildOutOfProc { get; set; } = false; + public bool UseMSBuildServer { get; set; } = false; + public string? ConfigureMSBuildTerminalLogger { get; set; } + public bool DisablePublishAndPackRelease { get; set; } = false; + public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_RUN_MSBUILD_OUTOFPROC` → `RunMSBuildOutOfProc` +- `DOTNET_CLI_USE_MSBUILD_SERVER` → `UseMSBuildServer` +- `DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER` → `ConfigureMSBuildTerminalLogger` +- `DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE` → `DisablePublishAndPackRelease` +- `DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS` → `LazyPublishAndPackReleaseForSolutions` + +#### 4. **SDK Resolver Configuration** +Settings that control SDK resolution and discovery: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class SdkResolverConfiguration +{ + public bool EnableLog { get; set; } = false; + public string? SdksDirectory { get; set; } + public string? SdksVersion { get; set; } + public string? CliDirectory { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG` → `EnableLog` +- `DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR` → `SdksDirectory` +- `DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER` → `SdksVersion` +- `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `CliDirectory` + +#### 5. **Workload Configuration** +Settings that control workload management and behavior: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class WorkloadConfiguration +{ + public bool UpdateNotifyDisable { get; set; } = false; + public int UpdateNotifyIntervalHours { get; set; } = 24; + public bool DisablePackGroups { get; set; } = false; + public bool SkipIntegrityCheck { get; set; } = false; + public string[]? ManifestRoots { get; set; } + public string[]? PackRoots { get; set; } + public bool ManifestIgnoreDefaultRoots { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE` → `UpdateNotifyDisable` +- `DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS` → `UpdateNotifyIntervalHours` +- `DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS` → `DisablePackGroups` +- `DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK` → `SkipIntegrityCheck` +- `DOTNETSDK_WORKLOAD_MANIFEST_ROOTS` → `ManifestRoots` +- `DOTNETSDK_WORKLOAD_PACK_ROOTS` → `PackRoots` +- `DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS` → `ManifestIgnoreDefaultRoots` + +#### 6. **First Time Use Configuration** +Settings that control first-time user experience setup: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class FirstTimeUseConfiguration +{ + public bool GenerateAspNetCertificate { get; set; } = true; + public bool AddGlobalToolsToPath { get; set; } = true; + public bool SkipFirstTimeExperience { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_GENERATE_ASPNET_CERTIFICATE` → `GenerateAspNetCertificate` +- `DOTNET_ADD_GLOBAL_TOOLS_TO_PATH` → `AddGlobalToolsToPath` +- `DOTNET_SKIP_FIRST_TIME_EXPERIENCE` → `SkipFirstTimeExperience` + +#### 7. **Development and Debugging Configuration** +Settings that control development tools and debugging features: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class DevelopmentConfiguration +{ + public bool PerfLogEnabled { get; set; } = false; + public string? PerfLogCount { get; set; } + public string? CliHome { get; set; } + public bool ContextVerbose { get; set; } = false; + public bool AllowTargetingPackCaching { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_PERF_LOG` → `PerfLogEnabled` +- `DOTNET_PERF_LOG_COUNT` → `PerfLogCount` +- `DOTNET_CLI_HOME` → `CliHome` +- `DOTNET_CLI_CONTEXT_VERBOSE` → `ContextVerbose` +- `DOTNETSDK_ALLOW_TARGETING_PACK_CACHING` → `AllowTargetingPackCaching` + +#### 8. **Tool and Global Tool Configuration** +Settings that control global tools behavior: + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class ToolConfiguration +{ + public bool AllowManifestInRoot { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT` → `AllowManifestInRoot` + +### Strongly-Typed Configuration Service with Source Generator + +```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +public interface DotNetConfigurationService +{ + IConfiguration RawConfiguration { get; } + + // Strongly-typed configuration access + CliUserExperienceConfiguration CliUserExperience { get; } + RuntimeHostConfiguration RuntimeHost { get; } + BuildConfiguration Build { get; } + SdkResolverConfiguration SdkResolver { get; } + WorkloadConfiguration Workload { get; } + FirstTimeUseConfiguration FirstTimeUse { get; } + DevelopmentConfiguration Development { get; } + ToolConfiguration Tool { get; } +} + +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +public class DotNetConfigurationService : DotNetConfigurationService +{ + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + + public DotNetConfigurationService(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + _configuration.GetSection("CliUserExperience").Get() ?? new()); + _runtimeHost = new Lazy(() => + _configuration.GetSection("RuntimeHost").Get() ?? new()); + _build = new Lazy(() => + _configuration.GetSection("Build").Get() ?? new()); + _sdkResolver = new Lazy(() => + _configuration.GetSection("SdkResolver").Get() ?? new()); + _workload = new Lazy(() => + _configuration.GetSection("Workload").Get() ?? new()); + _firstTimeUse = new Lazy(() => + _configuration.GetSection("FirstTimeUse").Get() ?? new()); + _development = new Lazy(() => + _configuration.GetSection("Development").Get() ?? new()); + _tool = new Lazy(() => + _configuration.GetSection("Tool").Get() ?? new()); + } +} +``` + + RuntimeHost = new RuntimeHostConfiguration(); + configuration.GetSection("dotnet:host").Bind(RuntimeHost); + + Build = new BuildConfiguration(); + configuration.GetSection("dotnet:build").Bind(Build); + + SdkResolver = new SdkResolverConfiguration(); + configuration.GetSection("dotnet:sdkresolver").Bind(SdkResolver); + + Workload = new WorkloadConfiguration(); + configuration.GetSection("dotnet:workload").Bind(Workload); + + FirstTimeUse = new FirstTimeUseConfiguration(); + configuration.GetSection("dotnet:firsttime").Bind(FirstTimeUse); + + Development = new DevelopmentConfiguration(); + configuration.GetSection("dotnet:development").Bind(Development); + + Tool = new ToolConfiguration(); + configuration.GetSection("dotnet:tool").Bind(Tool); + } +} +``` + +### Benefits of Strongly-Typed Configuration with Lazy Initialization + +1. **Minimal Startup Cost**: Lazy initialization means configuration binding only happens when properties are actually accessed +2. **Native AOT and Trim-Friendly**: Source generator eliminates reflection, making the code compatible with Native AOT compilation and trimming +3. **Performance**: Generated code is faster than reflection-based binding at runtime, and lazy loading reduces unnecessary work +4. **Memory Efficiency**: Configuration sections are only allocated when needed, reducing memory pressure during startup +5. **Intellisense and Compile-time Safety**: Developers get full IDE support and compile-time checking +6. **Logical Grouping**: Related settings are grouped together functionally +7. **Type Safety**: Boolean values are actual booleans, integers are integers, etc. +8. **Default Values**: Clear default values are defined in the model classes +9. **Discoverability**: Developers can explore configuration options through the object model +10. **Validation**: Can add data annotations for validation +11. **Documentation**: Each property can have XML documentation +12. **Source Generation**: Using `ConfigurationBinder.Get()` with `EnableConfigurationBindingGenerator=true` generates efficient binding code + +### Usage Examples with Lazy Initialization + +```csharp +// Fast startup - configuration service creation is very lightweight +var config = DotNetConfiguration.CreateTyped(); + +// First access triggers lazy binding of only the CliUserExperience section +if (config.CliUserExperience.TelemetryOptOut) +{ + // Skip telemetry - only this section is bound, others remain uninitialized +} + +// Subsequent access to the same section is fast (cached) +bool noLogo = config.CliUserExperience.NoLogo; + +// Other sections are only bound when first accessed +string? hostPath = config.RuntimeHost.HostPath; // RuntimeHost section bound here +bool enableLogs = config.SdkResolver.EnableLog; // SdkResolver section bound here + +// For scenarios that only need environment variables (fastest startup): +var minimalConfig = DotNetConfiguration.CreateMinimal(); +if (minimalConfig.CliUserExperience.TelemetryOptOut) +{ + // Only environment variable provider was initialized +} + +// Instead of error-prone string-based access: +// bool telemetryOptOut = configService.GetBoolValue("dotnet:cli:telemetry:optout", false); +``` + +This approach eliminates the need to remember canonical key names and provides a much more developer-friendly API. + +## Error Handling + +- **Malformed global.json**: Log warning and continue without global.json configuration +- **Missing files**: Silently ignore missing optional configuration files +- **Configuration access errors**: Provide meaningful error messages and fallback to defaults + +## Future Enhancements + +### dotnet.config Support + +A future enhancement could add support for a `dotnet.config` INI file that provides project-specific configuration: + +```ini +[cli] +telemetryOptOut=true +noLogo=true + +[build] +configuration=Release +``` + +This would map to configuration keys like: +- `cli:telemetryOptOut` → `"true"` +- `cli:noLogo` → `"true"` +- `build:configuration` → `"Release"` + +### Configuration Schema Validation + +Consider adding schema validation for configuration files to provide better error messages. For INI files, this would involve validating known sections and keys. + +### Configuration Hot Reload + +For development scenarios, consider adding support for configuration hot reload when files change. + +## Breaking Changes + +This refactoring should not introduce any breaking changes as it maintains backward compatibility with: +- All existing DOTNET_ prefixed environment variables +- All system environment variables (accessed through fallback provider) +- All existing global.json file structures and locations +- All existing APIs and interfaces (through adapter patterns) + + +## Success Criteria + +- [ ] **Microsoft.Extensions.Configuration.DotnetCli project created with proper structure** +- [ ] **Configuration binding source generator enabled (`EnableConfigurationBindingGenerator=true`)** +- [ ] **All configuration models are `sealed` classes optimized for source generator** +- [ ] **Lazy initialization implemented for all configuration sections** +- [ ] **Unused configuration sections have zero runtime cost** +- [ ] **Strongly-typed configuration binding using `ConfigurationBinder.Get()` throughout** +- [ ] All direct `Environment.GetEnvironmentVariable()` calls for DOTNET_ prefixed variables replaced +- [ ] All direct global.json reading replaced with configuration providers +- [ ] System environment variables continue to work through existing providers +- [ ] **Functional grouping of configuration settings implemented** +- [ ] **Type-safe configuration access with compile-time checking** +- [ ] **AOT and trim-friendly configuration system (no reflection)** +- [ ] Comprehensive test coverage for new configuration system including source generator scenarios +- [ ] All existing functionality preserved (backward compatibility) +- [ ] **Performance improved due to lazy loading and source generation** +- [ ] Documentation updated to reflect strongly-typed configuration system diff --git a/eng/Versions.props b/eng/Versions.props index 99e97cedaae8..0282cc1656a5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -103,6 +103,10 @@ 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 40f0aa86128e..a023ab9fa097 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -165,7 +165,7 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { # Disable telemetry on CI. if ($ci) { - $env:DOTNET_CLI_TELEMETRY_OPTOUT=1 + $env:DOTNET_CLI_TELEMETRY_OPTOUT=$true } # Find the first path on %PATH% that contains the dotnet.exe @@ -293,7 +293,7 @@ function InstallDotNet([string] $dotnetRoot, if ($runtime -eq "aspnetcore") { $runtimePath = $runtimePath + "\Microsoft.AspNetCore.App" } if ($runtime -eq "windowsdesktop") { $runtimePath = $runtimePath + "\Microsoft.WindowsDesktop.App" } $runtimePath = $runtimePath + "\" + $version - + $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" if (Test-Path $runtimePath) { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 3def02a638d2..dc16d2adf5d6 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -123,7 +123,7 @@ function InitializeDotNetCli { # Disable telemetry on CI if [[ $ci == true ]]; then - export DOTNET_CLI_TELEMETRY_OPTOUT=1 + export DOTNET_CLI_TELEMETRY_OPTOUT=true fi # LTTNG is the logging infrastructure used by Core CLR. Need this variable set diff --git a/sdk.slnx b/sdk.slnx index 09939f5d6b46..99cb8e704055 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -27,6 +27,7 @@ + diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 8e1b4aa8b81d..1ab9f223d40a 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -5,6 +5,7 @@ using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Pack; @@ -31,8 +32,7 @@ public static PackCommand FromParseResult(ParseResult parseResult, string? msbui new ReleasePropertyProjectLocator.DependentCommandOptions( parseResult.GetValue(PackCommandParser.SlnOrProjectArgument), parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null - ) - ); + )); bool noRestore = parseResult.HasOption(PackCommandParser.NoRestoreOption) || parseResult.HasOption(PackCommandParser.NoBuildOption); var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments( diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs index 8a504a9e01aa..e393fd389e6c 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.CommandLine.Completions; using System.CommandLine.Parsing; using Microsoft.DotNet.Cli.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; @@ -14,36 +13,6 @@ namespace Microsoft.DotNet.Cli.Commands.Package.Add; public static class PackageAddCommandParser { - public static readonly Argument CmdPackageArgument = CommonArguments.RequiredPackageIdentityArgument() - .AddCompletions((context) => - { - // we should take --prerelease flags into account for version completion - var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); - return QueryNuGet(context.WordToComplete, allowPrerelease, CancellationToken.None).Result.Select(packageId => new CompletionItem(packageId)); - }); - - public static readonly Option VersionOption = new DynamicForwardedOption("--version", "-v") - { - Description = CliCommandStrings.CmdVersionDescription, - HelpName = CliCommandStrings.CmdVersion - }.ForwardAsSingle(o => $"--version {o}") - .AddCompletions((context) => - { - // we can only do version completion if we have a package id - if (context.ParseResult.GetValue(CmdPackageArgument) is { HasVersion: false } packageId) - { - // we should take --prerelease flags into account for version completion - var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); - return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None) - .Result - .Select(version => new CompletionItem(version.ToNormalizedString())); - } - else - { - return []; - } - }); - public static readonly Option FrameworkOption = new ForwardedOption("--framework", "-f") { Description = CliCommandStrings.PackageAddCmdFrameworkDescription, @@ -76,6 +45,36 @@ public static class PackageAddCommandParser Arity = ArgumentArity.Zero }.ForwardAs("--prerelease"); + public static readonly Argument CmdPackageArgument = CommonArguments.RequiredPackageIdentityArgument() + .AddCompletions((context) => + { + // we should take --prerelease flags into account for version completion + var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); + return QueryNuGet(context.WordToComplete, allowPrerelease, CancellationToken.None).Result.Select(packageId => new CompletionItem(packageId)); + }); + + public static readonly Option VersionOption = new DynamicForwardedOption("--version", "-v") + { + Description = CliCommandStrings.CmdVersionDescription, + HelpName = CliCommandStrings.CmdVersion + }.ForwardAsSingle(o => $"--version {o}") + .AddCompletions((context) => + { + // we can only do version completion if we have a package id + if (context.ParseResult.GetValue(CmdPackageArgument) is { HasVersion: false } packageId) + { + // we should take --prerelease flags into account for version completion + var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); + return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None) + .Result + .Select(version => new CompletionItem(version.ToNormalizedString())); + } + else + { + return []; + } + }); + private static readonly Command Command = ConstructCommand(); public static Command GetCommand() @@ -105,13 +104,14 @@ private static Command ConstructCommand() private static void DisallowVersionIfPackageIdentityHasVersionValidator(OptionResult result) { - if (result.Parent.GetValue(CmdPackageArgument).HasVersion) + if (result.Parent!.GetValue(CmdPackageArgument).HasVersion) { result.AddError(CliCommandStrings.ValidationFailedDuplicateVersion); } } - public static async Task> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken) + // Only called during tab-completions, so this is allowed can hack/create singleton members like the configuration/downloader + internal static async Task> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken) { try { @@ -125,6 +125,7 @@ public static async Task> QueryNuGet(string packageStem, boo } } + // Only called during tab-completions, so this is allowed can hack/create singleton members like the configuration/downloader internal static async Task> QueryVersionsForPackage(string packageId, string versionFragment, bool allowPrerelease, CancellationToken cancellationToken) { try diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index eab6dee108b3..f472fb805dfc 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -6,6 +6,7 @@ using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Publish; diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 9cf9b5e7bafe..3358c9471eb0 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Extensions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -164,51 +165,7 @@ public static Command GetCommand() return Command; } - public static string GetTestRunnerName() - { - var builder = new ConfigurationBuilder(); - - string? dotnetConfigPath = GetDotnetConfigPath(Environment.CurrentDirectory); - if (!File.Exists(dotnetConfigPath)) - { - return CliConstants.VSTest; - } - - builder.AddIniFile(dotnetConfigPath); - - IConfigurationRoot config = builder.Build(); - var testSection = config.GetSection("dotnet.test.runner"); - - if (!testSection.Exists()) - { - return CliConstants.VSTest; - } - - string? runnerNameSection = testSection["name"]; - - if (string.IsNullOrEmpty(runnerNameSection)) - { - return CliConstants.VSTest; - } - - return runnerNameSection; - } - - private static string? GetDotnetConfigPath(string? startDir) - { - string? directory = startDir; - while (directory != null) - { - string dotnetConfigPath = Path.Combine(directory, "dotnet.config"); - if (File.Exists(dotnetConfigPath)) - { - return dotnetConfigPath; - } - - directory = Path.GetDirectoryName(directory); - } - return null; - } + public static string GetTestRunnerName() => DotNetConfigurationFactory.Create().Test.RunnerName; private static Command ConstructCommand() { diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index 0a23fad43910..488f8589b1a8 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; -using System.Transactions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; @@ -16,9 +13,9 @@ using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.Commands.Tool.Update; -using Microsoft.DotNet.Cli.Commands.Tool.Common; using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; using Microsoft.DotNet.Cli.Commands.Tool.List; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Tool.Install; @@ -26,7 +23,7 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.Install; internal delegate (IToolPackageStore, IToolPackageStoreQuery, IToolPackageDownloader) CreateToolPackageStoresAndDownloader( DirectoryPath? nonGlobalLocation = null, - IEnumerable forwardRestoreArguments = null); + IEnumerable? forwardRestoreArguments = null); internal class ToolInstallGlobalOrToolPathCommand : CommandBase { @@ -35,22 +32,22 @@ internal class ToolInstallGlobalOrToolPathCommand : CommandBase private readonly CreateShellShimRepository _createShellShimRepository; private readonly CreateToolPackageStoresAndDownloaderAndUninstaller _createToolPackageStoreDownloaderUninstaller; private readonly ShellShimTemplateFinder _shellShimTemplateFinder; - private readonly IToolPackageStoreQuery _store; + private readonly IToolPackageStoreQuery? _store; private readonly PackageId? _packageId; - private readonly string _configFilePath; - private readonly string _framework; - private readonly string[] _source; - private readonly string[] _addSource; + private readonly string? _configFilePath; + private readonly string? _framework; + private readonly string[]? _source; + private readonly string[]? _addSource; private readonly bool _global; private readonly VerbosityOptions _verbosity; - private readonly string _toolPath; - private readonly string _architectureOption; + private readonly string? _toolPath; + private readonly string? _architectureOption; private readonly IEnumerable _forwardRestoreArguments; private readonly bool _allowRollForward; private readonly bool _allowPackageDowngrade; private readonly bool _updateAll; - private readonly string _currentWorkingDirectory; + private readonly string? _currentWorkingDirectory; private readonly bool? _verifySignatures; internal readonly RestoreActionConfig restoreActionConfig; @@ -58,13 +55,13 @@ internal class ToolInstallGlobalOrToolPathCommand : CommandBase public ToolInstallGlobalOrToolPathCommand( ParseResult parseResult, PackageId? packageId = null, - CreateToolPackageStoresAndDownloaderAndUninstaller createToolPackageStoreDownloaderUninstaller = null, - CreateShellShimRepository createShellShimRepository = null, - IEnvironmentPathInstruction environmentPathInstruction = null, - IReporter reporter = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IToolPackageStoreQuery store = null, - string currentWorkingDirectory = null, + CreateToolPackageStoresAndDownloaderAndUninstaller? createToolPackageStoreDownloaderUninstaller = null, + CreateShellShimRepository? createShellShimRepository = null, + IEnvironmentPathInstruction? environmentPathInstruction = null, + IReporter? reporter = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IToolPackageStoreQuery? store = null, + string? currentWorkingDirectory = null, bool? verifySignatures = null) : base(parseResult) { @@ -138,7 +135,9 @@ public override int Execute() } else { - return ExecuteInstallCommand((PackageId)_packageId); + // this is safe because the parser definition will never let there be a + // case where updateAll isn't passed _and_ no packageId is passed + return ExecuteInstallCommand((PackageId)_packageId!); } } @@ -162,7 +161,7 @@ private int ExecuteInstallCommand(PackageId packageId) var appHostSourceDirectory = ShellShimTemplateFinder.GetDefaultAppHostSourceDirectory(); IShellShimRepository shellShimRepository = _createShellShimRepository(appHostSourceDirectory, toolPath); - IToolPackage oldPackageNullable = GetOldPackage(toolPackageStoreQuery, packageId); + IToolPackage? oldPackageNullable = GetOldPackage(toolPackageStoreQuery, packageId); if (oldPackageNullable != null) { @@ -172,7 +171,7 @@ private int ExecuteInstallCommand(PackageId packageId) { _reporter.WriteLine(string.Format(CliCommandStrings.ToolAlreadyInstalled, oldPackageNullable.Id, oldPackageNullable.Version.ToNormalizedString()).Green()); return 0; - } + } } TransactionalAction.Run(() => @@ -202,7 +201,7 @@ private int ExecuteInstallCommand(PackageId packageId) EnsureVersionIsHigher(oldPackageNullable, newInstalledPackage, _allowPackageDowngrade); - NuGetFramework framework; + NuGetFramework? framework; if (string.IsNullOrEmpty(_framework) && newInstalledPackage.Frameworks.Count() > 0) { framework = newInstalledPackage.Frameworks @@ -250,7 +249,7 @@ private static bool ToolVersionAlreadyInstalled(IToolPackage oldPackageNullable, return oldPackageNullable != null && oldPackageNullable.Version == nuGetVersion; } - private static void EnsureVersionIsHigher(IToolPackage oldPackageNullable, IToolPackage newInstalledPackage, bool allowDowngrade) + private static void EnsureVersionIsHigher(IToolPackage? oldPackageNullable, IToolPackage newInstalledPackage, bool allowDowngrade) { if (oldPackageNullable != null && newInstalledPackage.Version < oldPackageNullable.Version && !allowDowngrade) { @@ -303,7 +302,7 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag { try { - uninstallAction(); + uninstallAction(); } catch (Exception ex) when (ToolUninstallCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex)) @@ -333,9 +332,9 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag return configFile; } - private static IToolPackage GetOldPackage(IToolPackageStoreQuery toolPackageStoreQuery, PackageId packageId) + private static IToolPackage? GetOldPackage(IToolPackageStoreQuery toolPackageStoreQuery, PackageId packageId) { - IToolPackage oldPackageNullable; + IToolPackage? oldPackageNullable; try { oldPackageNullable = toolPackageStoreQuery.EnumeratePackageVersions(packageId).SingleOrDefault(); @@ -355,7 +354,7 @@ private static IToolPackage GetOldPackage(IToolPackageStoreQuery toolPackageStor return oldPackageNullable; } - private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstalledPackage) + private void PrintSuccessMessage(IToolPackage? oldPackage, IToolPackage newInstalledPackage) { if (!_verbosity.IsQuiet()) { @@ -381,7 +380,7 @@ private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstal { _reporter.WriteLine( string.Format( - + newInstalledPackage.Version.IsPrerelease ? CliCommandStrings.UpdateSucceededPreVersionNoChange : CliCommandStrings.UpdateSucceededStableVersionNoChange, newInstalledPackage.Id, newInstalledPackage.Version).Green()); diff --git a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs index 99681825c16f..046b7ba6c8d8 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Collections.Concurrent; +using System.Diagnostics; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; @@ -12,6 +11,7 @@ using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.NativeWrapper; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -30,15 +30,15 @@ internal class FileBasedInstaller : IInstaller private const string InstalledWorkloadSetsDir = "InstalledWorkloadSets"; private const string HistoryDir = "history"; protected readonly string _dotnetDir; - protected readonly string _userProfileDir; + protected readonly string? _userProfileDir; protected readonly string _workloadRootDir; protected readonly DirectoryPath _tempPackagesDir; private readonly INuGetPackageDownloader _nugetPackageDownloader; private IWorkloadResolver _workloadResolver; private readonly SdkFeatureBand _sdkFeatureBand; private readonly FileBasedInstallationRecordRepository _installationRecordRepository; - private readonly PackageSourceLocation _packageSourceLocation; - private readonly RestoreActionConfig _restoreActionConfig; + private readonly PackageSourceLocation? _packageSourceLocation; + private readonly RestoreActionConfig? _restoreActionConfig; public int ExitCode => 0; @@ -46,22 +46,25 @@ public FileBasedInstaller(IReporter reporter, SdkFeatureBand sdkFeatureBand, IWorkloadResolver workloadResolver, string userProfileDir, - INuGetPackageDownloader nugetPackageDownloader = null, - string dotnetDir = null, - string tempDirPath = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + string? dotnetDir = null, + string? tempDirPath = null, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - RestoreActionConfig restoreActionConfig = null, + PackageSourceLocation? packageSourceLocation = null, + RestoreActionConfig? restoreActionConfig = null, VerbosityOptions nugetPackageDownloaderVerbosity = VerbosityOptions.normal) { _userProfileDir = userProfileDir; - _dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); + _dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath)!; _tempPackagesDir = new DirectoryPath(tempDirPath ?? PathUtilities.CreateTempSubdirectory()); ILogger logger = verbosity.IsDetailedOrDiagnostic() ? new NuGetConsoleLogger() : new NullLogger(); _restoreActionConfig = restoreActionConfig; _nugetPackageDownloader = nugetPackageDownloader ?? - new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), logger, + new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, + DotNetConfigurationFactory.Create(), + filePermissionSetter: null, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, restoreActionConfig: _restoreActionConfig, verbosityOptions: nugetPackageDownloaderVerbosity); bool userLocal = WorkloadFileBasedInstall.IsUserLocal(_dotnetDir, sdkFeatureBand.ToString()); @@ -92,10 +95,10 @@ IEnumerable GetPacksInWorkloads(IEnumerable workloadIds) .Select(packId => _workloadResolver.TryGetPackInfo(packId)) .Where(pack => pack != null); - return packs; + return packs!; } - public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) + public WorkloadSet? InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) { string workloadSetPackageVersion = WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out SdkFeatureBand workloadSetFeatureBand); var workloadSetPackageId = GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand); @@ -123,9 +126,9 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo return WorkloadSet.FromWorkloadSetFolder(workloadSetPath, workloadSetVersion, _sdkFeatureBand); } - public WorkloadSet GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult(); + public WorkloadSet? GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult(); - public async Task GetWorkloadSetContentsAsync(string workloadSetVersion) + public async Task GetWorkloadSetContentsAsync(string workloadSetVersion) { string workloadSetPackageVersion = WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand); var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand), @@ -191,7 +194,7 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand { if (!Directory.Exists(Path.GetDirectoryName(packInfo.Path))) { - Directory.CreateDirectory(Path.GetDirectoryName(packInfo.Path)); + Directory.CreateDirectory(Path.GetDirectoryName(packInfo.Path)!); } if (IsSingleFilePack(packInfo)) @@ -272,6 +275,8 @@ string GetManifestInstallDirForFeatureBand(string sdkFeatureBand) public void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITransactionContext transactionContext, DirectoryPath? offlineCache = null) { + Debug.Assert(manifestUpdate.NewFeatureBand is not null, "Manifest update must have a feature band"); + Debug.Assert(manifestUpdate.NewVersion is not null, "Manifest update must have a version"); var newManifestPath = Path.Combine(GetManifestInstallDirForFeatureBand(manifestUpdate.NewFeatureBand), manifestUpdate.ManifestId.ToString(), manifestUpdate.NewVersion.ToString()); _reporter.WriteLine(string.Format(CliCommandStrings.InstallingWorkloadManifest, manifestUpdate.ManifestId, manifestUpdate.NewVersion)); @@ -300,8 +305,8 @@ public void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITrans void InstallPackage(PackageId packageId, string packageVersion, string targetFolder, ITransactionContext transactionContext, DirectoryPath? offlineCache) { - string packagePath = null; - string tempBackupDir = null; + string? packagePath = null; + string? tempBackupDir = null; bool directoryExists = Directory.Exists(targetFolder) && Directory.GetFileSystemEntries(targetFolder).Any(); transactionContext.Run( @@ -403,7 +408,8 @@ public void GarbageCollect(Func getResolverForWorkloa var featureBandsWithWorkloadInstallRecords = _installationRecordRepository.GetFeatureBandsWithInstallationRecords(); - var installedSdkFeatureBands = NETCoreSdkResolverNativeWrapper.GetAvailableSdks(_dotnetDir).Select(sdkDir => new SdkFeatureBand(Path.GetFileName(sdkDir))).ToHashSet(); + // If we can't get SDKs, something has gone _very_ wrong. That's why we ! here. + var installedSdkFeatureBands = NETCoreSdkResolverNativeWrapper.GetAvailableSdks(_dotnetDir)!.Select(sdkDir => new SdkFeatureBand(Path.GetFileName(sdkDir))).ToHashSet(); // Tests will often use a dotnet folder without any SDKs installed. To work around this, always add the current feature band to the list of installed feature bands installedSdkFeatureBands.Add(_sdkFeatureBand); @@ -418,8 +424,7 @@ public void GarbageCollect(Func getResolverForWorkloa // Get the feature band of the workload set WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand); - List referencingFeatureBands; - if (!workloadSetInstallRecords.TryGetValue((workloadSetVersion, workloadSetFeatureBand), out referencingFeatureBands)) + if (!workloadSetInstallRecords.TryGetValue((workloadSetVersion, workloadSetFeatureBand), out List? referencingFeatureBands)) { // If there are no install records for a workload set that is on disk, then ignore it. It is probably a baseline workload set. continue; @@ -535,18 +540,18 @@ public void GarbageCollect(Func getResolverForWorkloa File.Delete(GetPackInstallRecordPath(packId, packVersion, featureBand)); } - var installationRecordDirectory = Path.GetDirectoryName(GetPackInstallRecordPath(packId, packVersion, featureBandsToRemove.First())); + var installationRecordDirectory = Path.GetDirectoryName(GetPackInstallRecordPath(packId, packVersion, featureBandsToRemove.First()))!; if (!Directory.GetFileSystemEntries(installationRecordDirectory).Any()) { // There are no installation records for the workload pack anymore, so we can delete the pack - var packToDelete = JsonSerializer.Deserialize(jsonPackInfo); + var packToDelete = JsonSerializer.Deserialize(jsonPackInfo)!; DeletePack(packToDelete); // Delete now-empty pack installation record directory Directory.Delete(installationRecordDirectory); // And delete the parent directory if it's also empty - string packIdDirectory = Path.GetDirectoryName(installationRecordDirectory); + string packIdDirectory = Path.GetDirectoryName(installationRecordDirectory)!; if (!Directory.GetFileSystemEntries(packIdDirectory).Any()) { Directory.Delete(packIdDirectory); @@ -587,7 +592,7 @@ public void UpdateInstallMode(SdkFeatureBand sdkFeatureBand, bool? newMode) private void UpdateInstallState(SdkFeatureBand sdkFeatureBand, Action update) { string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(sdkFeatureBand, _workloadRootDir), "default.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); var installStateContents = InstallStateContents.FromPath(path); update(installStateContents); File.WriteAllText(path, installStateContents.ToString()); @@ -642,7 +647,7 @@ public IEnumerable GetWorkloadHistoryRecords(string sdkFe public void Shutdown() { // Perform any additional cleanup here that's intended to run at the end of the command, regardless - // of success or failure. For file based installs, there shouldn't be any additional work to + // of success or failure. For file based installs, there shouldn't be any additional work to // perform. } @@ -674,7 +679,7 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) { Directory.Delete(targetPath, true); } - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); FileAccessRetrier.RetryOnMoveAccessFailure(() => DirectoryPath.MoveDirectory(Path.Combine(extractionPath, "data"), targetPath)); } finally @@ -710,7 +715,7 @@ private void DeletePack(PackInfo packInfo) else { Directory.Delete(packInfo.Path, true); - var packIdDir = Path.GetDirectoryName(packInfo.Path); + var packIdDir = Path.GetDirectoryName(packInfo.Path)!; if (!Directory.EnumerateFileSystemEntries(packIdDir).Any()) { Directory.Delete(packIdDir, true); @@ -728,7 +733,7 @@ string GetWorkloadSetInstallRecordPath(string workloadSetVersion, SdkFeatureBand void WriteWorkloadSetInstallationRecord(string workloadSetVersion, SdkFeatureBand workloadSetFeatureBand, SdkFeatureBand referencingFeatureBand) { var path = GetWorkloadSetInstallRecordPath(workloadSetVersion, workloadSetFeatureBand, referencingFeatureBand); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var _ = File.Create(path); } @@ -779,7 +784,7 @@ private string GetManifestInstallRecordPath(ManifestId manifestId, ManifestVersi void WriteManifestInstallationRecord(ManifestId manifestId, ManifestVersion manifestVersion, SdkFeatureBand featureBand, SdkFeatureBand referencingFeatureBand) { var path = GetManifestInstallRecordPath(manifestId, manifestVersion, featureBand, referencingFeatureBand); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var _ = File.Create(path); } @@ -872,7 +877,7 @@ private void WritePackInstallationRecord(PackInfo packInfo, SdkFeatureBand featu var path = GetPackInstallRecordPath(new WorkloadPackId(packInfo.ResolvedPackageId), packInfo.Version, featureBand); if (!Directory.Exists(Path.GetDirectoryName(path))) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); } File.WriteAllText(path, JsonSerializer.Serialize(packInfo)); } @@ -884,12 +889,12 @@ private void DeletePackInstallationRecord(PackInfo packInfo, SdkFeatureBand feat { File.Delete(packInstallRecord); - var packRecordVersionDir = Path.GetDirectoryName(packInstallRecord); + var packRecordVersionDir = Path.GetDirectoryName(packInstallRecord)!; if (!Directory.GetFileSystemEntries(packRecordVersionDir).Any()) { Directory.Delete(packRecordVersionDir); - var packRecordIdDir = Path.GetDirectoryName(packRecordVersionDir); + var packRecordIdDir = Path.GetDirectoryName(packRecordVersionDir)!; if (!Directory.GetFileSystemEntries(packRecordIdDir).Any()) { Directory.Delete(packRecordIdDir); diff --git a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs index 26d7c3e27ee4..26578818e69b 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.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. -#nullable disable - using System.Globalization; using System.Runtime.Versioning; using Microsoft.DotNet.Cli.Commands.Workload.Config; @@ -12,6 +10,7 @@ using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.Win32.Msi; @@ -32,7 +31,7 @@ internal partial class NetSdkMsiInstallerClient : MsiInstallerBase, IInstaller private bool _shutdown; - private readonly PackageSourceLocation _packageSourceLocation; + private readonly PackageSourceLocation? _packageSourceLocation; private readonly string _dependent; @@ -43,10 +42,10 @@ public NetSdkMsiInstallerClient(InstallElevationContextBase elevationContext, bool verifySignatures, IWorkloadResolver workloadResolver, SdkFeatureBand sdkFeatureBand, - INuGetPackageDownloader nugetPackageDownloader = null, + INuGetPackageDownloader nugetPackageDownloader, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - IReporter reporter = null) : base(elevationContext, logger, verifySignatures, reporter) + PackageSourceLocation? packageSourceLocation = null, + IReporter? reporter = null) : base(elevationContext, logger, verifySignatures, reporter) { _packageSourceLocation = packageSourceLocation; _nugetPackageDownloader = nugetPackageDownloader; @@ -102,16 +101,16 @@ public IEnumerable GetDownloads(IEnumerable worklo } // Wrap the setup logger in an IReporter so it can be passed to the garbage collector - private class SetupLogReporter(ISetupLogger setupLogger) : IReporter + private class SetupLogReporter(ISetupLogger? setupLogger) : IReporter { - private readonly ISetupLogger _setupLogger = setupLogger; + private readonly ISetupLogger? _setupLogger = setupLogger; // SetupLogger doesn't have a way of writing a message that shouldn't include a newline. So if this method is used a message may be split across multiple lines, // but that's probably better than not writing a message at all or throwing an exception - public void Write(string message) => _setupLogger.LogMessage(message); - public void WriteLine(string message) => _setupLogger.LogMessage(message); - public void WriteLine() => _setupLogger.LogMessage(""); - public void WriteLine(string format, params object[] args) => _setupLogger.LogMessage(string.Format(format, args)); + public void Write(string message) => _setupLogger?.LogMessage(message); + public void WriteLine(string message) => _setupLogger?.LogMessage(message); + public void WriteLine() => _setupLogger?.LogMessage(""); + public void WriteLine(string format, params object?[] args) => _setupLogger?.LogMessage(string.Format(format, args)); } /// @@ -304,7 +303,7 @@ public void GarbageCollect(Func getResolverForWorkloa } } - public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache) + public WorkloadSet? InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache) { ReportPendingReboot(); @@ -313,7 +312,7 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo context.Run( action: () => { - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Install, installedVersion); if (plannedAction == InstallAction.Install) @@ -328,7 +327,7 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo }, rollback: () => { - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Uninstall, installedVersion); if (plannedAction == InstallAction.Uninstall) @@ -426,7 +425,7 @@ private void RemoveWorkloadSets(List workloadSetsToRemove, Di { foreach (WorkloadSetRecord record in workloadSetsToRemove) { - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { string msiNuGetPackageId = $"Microsoft.NET.Workloads.{record.WorkloadSetFeatureBand}.Msi.{HostArchitecture}"; @@ -452,7 +451,7 @@ private void RemoveWorkloadManifests(List manifestToRemo { foreach (WorkloadManifestRecord record in manifestToRemove) { - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { string msiNuGetPackageId = $"{record.ManifestId}.Manifest-{record.ManifestFeatureBand}.Msi.{HostArchitecture}"; @@ -482,7 +481,7 @@ private void RemoveWorkloadPacks(List packsToRemove, Directo // if a previous removal was interrupted. We can't safely clean up orphaned records because it's too expensive // to query all installed components and determine the product codes associated with the component that // created the record. - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { @@ -575,7 +574,7 @@ void InstallWorkloadManifestImplementation(ManifestVersionUpdate manifestUpdate, // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(msiPackageId, msiPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, action, installedVersion); ExecutePackage(msi, plannedAction, msiPackageId); @@ -595,7 +594,7 @@ public void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(aquirableMsi.NuGetPackageId, aquirableMsi.NuGetPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Repair, installedVersion); ExecutePackage(msi, plannedAction, aquirableMsi.NuGetPackageId); @@ -627,7 +626,7 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(msiToInstall.NuGetPackageId, msiToInstall.NuGetPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Install, installedVersion); if (plannedAction == InstallAction.Install) { @@ -688,7 +687,7 @@ void RollBackMsiInstall(WorkloadDownload msiToRollback, DirectoryPath? offlineCa } // Make sure the MSI is actually installed. - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Uninstall, installedVersion); // The previous steps would have logged the final action. If the verdict is not to uninstall we can exit. @@ -726,7 +725,10 @@ public void Shutdown() Log?.LogMessage("Shutdown completed."); Log?.LogMessage($"Restart required: {Restart}"); - ((TimestampedFileLogger)Log).Dispose(); + if (Log is TimestampedFileLogger t) + { + t.Dispose(); + } _shutdown = true; } @@ -762,7 +764,7 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) if (Directory.Exists(extractedManifestPath)) { Log?.LogMessage($"ExtractManifestAsync: Copying manifest from '{extractionPath}' to '{targetPath}'"); - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); FileAccessRetrier.RetryOnMoveAccessFailure(() => DirectoryPath.MoveDirectory(extractedManifestPath, targetPath)); } else @@ -794,8 +796,8 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) var manifestsFolder = Path.Combine(msiExtractionPath, "dotnet", "sdk-manifests"); - string manifestFolder = null; - string manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault(); + string? manifestFolder = null; + string? manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault(); if (manifestsFeatureBandFolder != null) { manifestFolder = Directory.GetDirectories(manifestsFeatureBandFolder).SingleOrDefault(); @@ -829,7 +831,7 @@ private void LogPackInfo(PackInfo packInfo) /// The product code of the MSI to detect. /// If detected, contains the version of the installed MSI. /// The detect state of the specified MSI. - private DetectState DetectPackage(string productCode, out Version installedVersion) + private DetectState DetectPackage(string productCode, out Version? installedVersion) { installedVersion = default; uint error = WindowsInstaller.GetProductInfo(productCode, InstallProperty.VERSIONSTRING, out string versionValue); @@ -859,7 +861,7 @@ private DetectState DetectPackage(string productCode, out Version installedVersi /// The MSI package to detect. /// If detected, contains the version of the installed MSI. /// The detect state of the specified MSI. - private DetectState DetectPackage(MsiPayload msi, out Version installedVersion) + private DetectState DetectPackage(MsiPayload msi, out Version? installedVersion) { return DetectPackage(msi.ProductCode, out installedVersion); } @@ -871,7 +873,7 @@ private DetectState DetectPackage(MsiPayload msi, out Version installedVersion) /// The detected state of the package. /// The requested action to perform. /// The action that will be performed. - private InstallAction PlanPackage(MsiPayload msi, DetectState state, InstallAction requestedAction, Version installedVersion) + private InstallAction PlanPackage(MsiPayload msi, DetectState state, InstallAction requestedAction, Version? installedVersion) { InstallAction plannedAction = InstallAction.None; @@ -964,7 +966,7 @@ private string ExtractPackage(string packageId, string packageVersion, Directory /// Gets a set of all the installed SDK feature bands. /// /// A List of all the installed SDK feature bands. - private static IEnumerable GetInstalledFeatureBands(ISetupLogger log = null) + private static IEnumerable GetInstalledFeatureBands(ISetupLogger? log = null) { HashSet installedFeatureBands = []; foreach (string sdkVersion in GetInstalledSdkVersions()) @@ -1018,7 +1020,7 @@ private MsiPayload GetCachedMsiPayload(string packageId, string packageVersion, /// The action to perform. /// A friendly name to display to the user when reporting progress. If no value is provided, the MSI /// filename will be used. - private void ExecutePackage(MsiPayload msi, InstallAction action, string displayName = null) + private void ExecutePackage(MsiPayload msi, InstallAction action, string? displayName = null) { uint error = Error.SUCCESS; string logFile = GetMsiLogName(msi, action); @@ -1071,7 +1073,7 @@ private uint ExecuteWithProgress(string progressLabel, Func installDelegat if (installTask.IsFaulted) { Reporter.WriteLine(" Failed"); - throw installTask.Exception.InnerException; + throw installTask.Exception.InnerException ?? installTask.Exception; } error = installTask.Result; @@ -1110,12 +1112,12 @@ public static NetSdkMsiInstallerClient Create( bool verifySignatures, SdkFeatureBand sdkFeatureBand, IWorkloadResolver workloadResolver, - INuGetPackageDownloader nugetPackageDownloader = null, + INuGetPackageDownloader? nugetPackageDownloader = null, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - IReporter reporter = null, - string tempDirPath = null, - RestoreActionConfig restoreActionConfig = null, + PackageSourceLocation? packageSourceLocation = null, + IReporter? reporter = null, + string? tempDirPath = null, + RestoreActionConfig? restoreActionConfig = null, bool shouldLog = true) { ISynchronizingLogger logger = @@ -1128,8 +1130,11 @@ public static NetSdkMsiInstallerClient Create( DirectoryPath tempPackagesDir = new(string.IsNullOrWhiteSpace(tempDirPath) ? PathUtilities.CreateTempSubdirectory() : tempDirPath); nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir, - filePermissionSetter: null, new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), restoreActionConfig: restoreActionConfig); + DotNetConfigurationFactory.Create(), + filePermissionSetter: null, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + restoreActionConfig: restoreActionConfig); } return new NetSdkMsiInstallerClient(elevationContext, logger, verifySignatures, workloadResolver, sdkFeatureBand, nugetPackageDownloader, @@ -1147,7 +1152,7 @@ private void ReportPendingReboot() } } - private void OnProcessExit(object sender, EventArgs e) + private void OnProcessExit(object? sender, EventArgs e) { if (!_shutdown) { diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs index 15049f909b09..97d75df7ec34 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.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. -#nullable disable - using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli.Extensions; @@ -10,6 +8,7 @@ using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -27,21 +26,21 @@ internal class WorkloadInstallCommand : InstallingWorkloadCommand public WorkloadInstallCommand( ParseResult parseResult, - IReporter reporter = null, - IWorkloadResolverFactory workloadResolverFactory = null, - IInstaller workloadInstaller = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IWorkloadManifestUpdater workloadManifestUpdater = null, - string tempDirPath = null, - IReadOnlyCollection workloadIds = null, + IReporter? reporter = null, + IWorkloadResolverFactory? workloadResolverFactory = null, + IInstaller? workloadInstaller = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IWorkloadManifestUpdater? workloadManifestUpdater = null, + string? tempDirPath = null, + IReadOnlyCollection? workloadIds = null, bool? skipWorkloadManifestUpdate = null) : base(parseResult, reporter: reporter, workloadResolverFactory: workloadResolverFactory, workloadInstaller: workloadInstaller, nugetPackageDownloader: nugetPackageDownloader, workloadManifestUpdater: workloadManifestUpdater, tempDirPath: tempDirPath, verbosityOptions: WorkloadInstallCommandParser.VerbosityOption) { _skipManifestUpdate = skipWorkloadManifestUpdate ?? parseResult.GetValue(WorkloadInstallCommandParser.SkipManifestUpdateOption); - var unprocessedWorkloadIds = workloadIds ?? parseResult.GetValue(WorkloadInstallCommandParser.WorkloadIdArgument); - if (unprocessedWorkloadIds?.Any(id => id.Contains('@')) == true) + IEnumerable unprocessedWorkloadIds = workloadIds ?? parseResult.GetRequiredValue(WorkloadInstallCommandParser.WorkloadIdArgument); + if (unprocessedWorkloadIds.Any(id => id.Contains('@')) == true) { _workloadIds = unprocessedWorkloadIds.Select(id => id.Split('@')[0]).ToList().AsReadOnly(); if (SpecifiedWorkloadSetVersionOnCommandLine) @@ -112,10 +111,11 @@ public override int Execute() { var packageDownloader = IsPackageDownloaderProvided ? PackageDownloader : new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - NullReporter.Instance, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: NullReporter.Instance, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures); @@ -206,7 +206,7 @@ public override int Execute() return _workloadInstaller.ExitCode; } - private void InstallWorkloads(WorkloadHistoryRecorder recorder, IReadOnlyCollection filteredWorkloadIds) + private void InstallWorkloads(WorkloadHistoryRecorder? recorder, IReadOnlyCollection filteredWorkloadIds) { // Normally we want to validate that the workload IDs specified were valid. However, if there is a global.json file with a workload // set version specified, and we might install that workload version, then we don't do that check here, because we might not have the right @@ -307,7 +307,7 @@ internal static void TryRunGarbageCollection(IInstaller workloadInstaller, IRepo } private async Task> GetPackageDownloadUrlsAsync(IEnumerable workloadIds, bool skipManifestUpdate, bool includePreview, - IReporter reporter = null, INuGetPackageDownloader packageDownloader = null) + IReporter? reporter = null, INuGetPackageDownloader? packageDownloader = null) { reporter ??= Reporter; packageDownloader ??= PackageDownloader; diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs index 8ed500786ec7..b5d16ec646c5 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.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. -#nullable disable - using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; using Microsoft.DotNet.Cli.Commands.Workload.List; @@ -11,6 +9,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.Configurer; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -28,8 +27,8 @@ internal class WorkloadManifestUpdater : IWorkloadManifestUpdater private readonly INuGetPackageDownloader _nugetPackageDownloader; private readonly SdkFeatureBand _sdkFeatureBand; private readonly string _userProfileDir; - private readonly PackageSourceLocation _packageSourceLocation; - private readonly Func _getEnvironmentVariable; + private readonly PackageSourceLocation? _packageSourceLocation; + private readonly Func _getEnvironmentVariable; private readonly IWorkloadInstallationRecordRepository _workloadRecordRepo; private readonly IWorkloadManifestInstaller _workloadManifestInstaller; private readonly bool _displayManifestUpdates; @@ -40,8 +39,8 @@ public WorkloadManifestUpdater(IReporter reporter, string userProfileDir, IWorkloadInstallationRecordRepository workloadRecordRepo, IWorkloadManifestInstaller workloadManifestInstaller, - PackageSourceLocation packageSourceLocation = null, - Func getEnvironmentVariable = null, + PackageSourceLocation? packageSourceLocation = null, + Func? getEnvironmentVariable = null, bool displayManifestUpdates = true, SdkFeatureBand? sdkFeatureBand = null) { @@ -60,16 +59,17 @@ public WorkloadManifestUpdater(IReporter reporter, private static WorkloadManifestUpdater GetInstance(string userProfileDir) { var reporter = new NullReporter(); - var dotnetPath = Path.GetDirectoryName(Environment.ProcessPath); + var dotnetPath = Path.GetDirectoryName(Environment.ProcessPath)!; var sdkVersion = Product.Version; var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(dotnetPath, sdkVersion, userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory)); var workloadResolver = WorkloadResolver.Create(workloadManifestProvider, dotnetPath, sdkVersion, userProfileDir); var tempPackagesDir = new DirectoryPath(PathUtilities.CreateTempSubdirectory()); var nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - reporter, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: reporter, verifySignatures: SignCheck.IsDotNetSigned()); var installer = WorkloadInstallerFactory.GetWorkloadInstaller(reporter, new SdkFeatureBand(sdkVersion), workloadResolver, VerbosityOptions.normal, userProfileDir, verifySignatures: false); @@ -86,7 +86,7 @@ public async Task UpdateAdvertisingManifestsAsync(bool includePreviews, bool use } else { - // this updates all the manifests + // this updates all the manifests var manifests = _workloadResolver.GetInstalledManifests(); await Task.WhenAll(manifests.Select(manifest => UpdateAdvertisingManifestAsync(manifest, includePreviews, offlineCache))).ConfigureAwait(false); WriteUpdatableWorkloadsFile(); @@ -140,7 +140,7 @@ private void WriteUpdatableWorkloadsFile() var jsonContent = JsonSerializer.Serialize(updatableWorkloads.Select(workload => workload.ToString()).ToArray()); if (Directory.Exists(Path.GetDirectoryName(filePath))) { - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); } File.WriteAllText(filePath, jsonContent); } @@ -177,7 +177,7 @@ public static void AdvertiseWorkloadUpdates() } } - public string GetAdvertisedWorkloadSetVersion() + public string? GetAdvertisedWorkloadSetVersion() { var advertisedPath = GetAdvertisingManifestPath(_sdkFeatureBand, new ManifestId(WorkloadSetManifestId)); var workloadSetVersionFilePath = Path.Combine(advertisedPath, Constants.workloadSetVersionFileName); @@ -234,7 +234,7 @@ public IEnumerable GetUpdatableWorkloadsToAdvertise(IEnumerable CalculateManifestRollbacks(string rollbackDefinitionFilePath, WorkloadHistoryRecorder recorder = null) + public IEnumerable CalculateManifestRollbacks(string rollbackDefinitionFilePath, WorkloadHistoryRecorder? recorder = null) { var currentManifestIds = GetInstalledManifestIds(); var manifestRollbacks = ParseRollbackDefinitionFile(rollbackDefinitionFilePath, _sdkFeatureBand); @@ -309,10 +309,10 @@ public async Task> GetManifestPackageDownloadsAsyn private IEnumerable GetInstalledManifestIds() => _workloadResolver.GetInstalledManifests().Select(manifest => new ManifestId(manifest.Id)); - private async Task UpdateManifestWithVersionAsync(string id, bool includePreviews, SdkFeatureBand band, NuGetVersion packageVersion = null, DirectoryPath? offlineCache = null) + private async Task UpdateManifestWithVersionAsync(string id, bool includePreviews, SdkFeatureBand band, NuGetVersion? packageVersion = null, DirectoryPath? offlineCache = null) { var manifestId = new ManifestId(id); - string packagePath = null; + string? packagePath = null; try { var manifestPackageId = _workloadManifestInstaller.GetManifestPackageId(manifestId, band); diff --git a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs index a8d35305f974..b059add3c698 100644 --- a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs @@ -1,14 +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 disable - using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -26,14 +25,14 @@ internal class WorkloadUpdateCommand : InstallingWorkloadCommand private readonly bool _shouldShutdownInstaller; public WorkloadUpdateCommand( ParseResult parseResult, - IReporter reporter = null, - IWorkloadResolverFactory workloadResolverFactory = null, - IInstaller workloadInstaller = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IWorkloadManifestUpdater workloadManifestUpdater = null, - string tempDirPath = null, + IReporter? reporter = null, + IWorkloadResolverFactory? workloadResolverFactory = null, + IInstaller? workloadInstaller = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IWorkloadManifestUpdater? workloadManifestUpdater = null, + string? tempDirPath = null, bool isRestoring = false, - WorkloadHistoryRecorder recorder = null, + WorkloadHistoryRecorder? recorder = null, bool? shouldUseWorkloadSetsFromGlobalJson = null) : base(parseResult, reporter: reporter, workloadResolverFactory: workloadResolverFactory, workloadInstaller: workloadInstaller, nugetPackageDownloader: nugetPackageDownloader, workloadManifestUpdater: workloadManifestUpdater, @@ -55,12 +54,15 @@ public WorkloadUpdateCommand( _workloadManifestUpdater = _workloadManifestUpdaterFromConstructor ?? new WorkloadManifestUpdater(resolvedReporter, _workloadResolver, PackageDownloader, _userProfileDir, _workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, sdkFeatureBand: _sdkFeatureBand); - _recorder = recorder; - if (_recorder is null) + + if (recorder is null) { _recorder = new(_workloadResolver, _workloadInstaller, () => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, null)); _recorder.HistoryRecord.CommandName = "update"; - + } + else + { + _recorder = recorder; } _fromHistorySpecified = parseResult.GetValue(WorkloadUpdateCommandParser.FromHistoryOption); @@ -85,10 +87,11 @@ public override int Execute() { var packageDownloader = IsPackageDownloaderProvided ? PackageDownloader : new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - NullReporter.Instance, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: NullReporter.Instance, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures); @@ -204,7 +207,7 @@ private async Task DownloadToOfflineCacheAsync(DirectoryPath offlineCache, bool await GetDownloads(GetUpdatableWorkloads(), skipManifestUpdate: false, includePreviews, offlineCache.Value); } - private async Task> GetUpdatablePackageUrlsAsync(bool includePreview, IReporter reporter = null, INuGetPackageDownloader packageDownloader = null) + private async Task> GetUpdatablePackageUrlsAsync(bool includePreview, IReporter? reporter = null, INuGetPackageDownloader? packageDownloader = null) { reporter ??= Reporter; packageDownloader ??= PackageDownloader; @@ -219,7 +222,7 @@ private async Task> GetUpdatablePackageUrlsAsync(bool includ return urls; } - private IEnumerable GetUpdatableWorkloads(IReporter reporter = null) + private IEnumerable? GetUpdatableWorkloads(IReporter? reporter = null) { reporter ??= Reporter; var workloads = FromHistory ? _WorkloadHistoryRecord.InstalledWorkloads.Select(s => new WorkloadId(s)) : GetInstalledWorkloads(_fromPreviousSdk); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs index 44b441349be3..bbd17f5a4916 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs @@ -121,9 +121,9 @@ public WorkloadCommandBase( PackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - nugetLogger, - Reporter, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: nugetLogger, + reporter: Reporter, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures, shouldUsePackageSourceMapping: true); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs index add9f8a7a261..accb3f4726be 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.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. -#nullable disable - using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index 8ac89af40212..fcb5c0ff30d5 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Collections.Concurrent; -using Microsoft.DotNet.Cli.NugetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Configuration; @@ -18,7 +16,6 @@ namespace Microsoft.DotNet.Cli.NuGetPackageDownloader; -// TODO: Never name a class the same name as the namespace. Update either for easier type resolution. internal class NuGetPackageDownloader : INuGetPackageDownloader { private readonly SourceCacheContext _cacheSettings; @@ -30,7 +27,6 @@ internal class NuGetPackageDownloader : INuGetPackageDownloader private readonly ILogger _verboseLogger; private readonly DirectoryPath _packageInstallDir; private readonly RestoreActionConfig _restoreActionConfig; - private readonly Func> _retryTimer; /// /// Reporter would output to the console regardless @@ -43,24 +39,24 @@ internal class NuGetPackageDownloader : INuGetPackageDownloader /// /// If true, the package downloader will verify the signatures of the packages it downloads. - /// Temporarily disabled for macOS and Linux. + /// Temporarily disabled for macOS and Linux. /// private readonly bool _verifySignatures; private readonly VerbosityOptions _verbosityOptions; - private readonly string _currentWorkingDirectory; + private readonly string? _currentWorkingDirectory; public NuGetPackageDownloader( DirectoryPath packageInstallDir, - IFilePermissionSetter filePermissionSetter = null, - IFirstPartyNuGetPackageSigningVerifier firstPartyNuGetPackageSigningVerifier = null, - ILogger verboseLogger = null, - IReporter reporter = null, - RestoreActionConfig restoreActionConfig = null, - Func> timer = null, + DotNetCliConfiguration? configurationService = null, + IFilePermissionSetter? filePermissionSetter = null, + IFirstPartyNuGetPackageSigningVerifier? firstPartyNuGetPackageSigningVerifier = null, + ILogger? verboseLogger = null, + IReporter? reporter = null, + RestoreActionConfig? restoreActionConfig = null, bool verifySignatures = false, bool shouldUsePackageSourceMapping = false, VerbosityOptions verbosityOptions = VerbosityOptions.normal, - string currentWorkingDirectory = null) + string? currentWorkingDirectory = null) { _currentWorkingDirectory = currentWorkingDirectory; _packageInstallDir = packageInstallDir; @@ -70,11 +66,9 @@ public NuGetPackageDownloader( new FirstPartyNuGetPackageSigningVerifier(); _filePermissionSetter = filePermissionSetter ?? new FilePermissionSetter(); _restoreActionConfig = restoreActionConfig ?? new RestoreActionConfig(); - _retryTimer = timer; _sourceRepositories = new(); - // If windows or env variable is set, verify signatures - _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true - : bool.TryParse(Environment.GetEnvironmentVariable(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification), out var shouldVerifySignature) ? shouldVerifySignature : OperatingSystem.IsLinux()); + // Use configuration service for signature verification + _verifySignatures = verifySignatures && (configurationService ?? DotNetConfigurationFactory.Create()).NuGet.SignatureVerificationEnabled; _cacheSettings = new SourceCacheContext { @@ -90,19 +84,19 @@ public NuGetPackageDownloader( } public async Task DownloadPackageAsync(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping? packageSourceMapping = null) { CancellationToken cancellationToken = CancellationToken.None; (var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion, packageSourceLocation, includePreview, includeUnlisted ?? packageVersion is not null, packageSourceMapping).ConfigureAwait(false); - FindPackageByIdResource resource = null; + FindPackageByIdResource? resource = null; SourceRepository repository = GetSourceRepository(source); resource = await repository.GetResourceAsync(cancellationToken) @@ -120,9 +114,9 @@ public async Task DownloadPackageAsync(PackageId packageId, throw new ArgumentException($"Package download folder must be specified either via {nameof(NuGetPackageDownloader)} constructor or via {nameof(downloadFolder)} method argument."); } var pathResolver = new VersionFolderPathResolver(resolvedDownloadFolder); - + string nupkgPath = pathResolver.GetPackageFilePath(packageId.ToString(), resolvedPackageVersion); - Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)); + Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)!); using FileStream destinationStream = File.Create(nupkgPath); bool success = await ExponentialRetry.ExecuteWithRetryOnFailure(async () => await resource.CopyNupkgToStreamAsync( @@ -138,7 +132,7 @@ public async Task DownloadPackageAsync(PackageId packageId, { throw new NuGetPackageInstallerException( string.Format("Downloading {0} version {1} failed", packageId, - packageVersion.ToNormalizedString())); + resolvedPackageVersion.ToNormalizedString())); } // Delete file if verification fails @@ -202,9 +196,9 @@ await repository.GetResourceAsync().ConfigureAwait( } } - public async Task GetPackageUrl(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + public async Task GetPackageUrl(PackageId packageId, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { (var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion, packageSourceLocation, includePreview).ConfigureAwait(false); @@ -218,10 +212,12 @@ public async Task GetPackageUrl(PackageId packageId, ); } - ServiceIndexResourceV3 serviceIndexResource = repository.GetResourceAsync().Result; - IReadOnlyList packageBaseAddress = - serviceIndexResource?.GetServiceEntryUris(ServiceTypes.PackageBaseAddress); - + ServiceIndexResourceV3? serviceIndexResource = await repository.GetResourceAsync(); + if (serviceIndexResource is null) + { + return null; + } + IReadOnlyList packageBaseAddress = serviceIndexResource.GetServiceEntryUris(ServiceTypes.PackageBaseAddress); return GetNupkgUrl(packageBaseAddress[0].ToString(), packageId, resolvedPackageVersion); } @@ -246,9 +242,8 @@ public async Task> ExtractPackageAsync(string packagePath, D if (!OperatingSystem.IsWindows()) { - string workloadUnixFilePermissions = allFilesInPackage.SingleOrDefault(p => - Path.GetRelativePath(targetFolder.Value, p).Equals("data/UnixFilePermissions.xml", - StringComparison.OrdinalIgnoreCase)); + string? workloadUnixFilePermissions = + allFilesInPackage.SingleOrDefault(p => Path.GetRelativePath(targetFolder.Value, p).Equals("data/UnixFilePermissions.xml", StringComparison.OrdinalIgnoreCase)); if (workloadUnixFilePermissions != default) { @@ -273,11 +268,11 @@ public async Task> GetLatestVersionsOfPackag } private async Task<(PackageSource, NuGetVersion)> GetPackageSourceAndVersion(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false, bool includeUnlisted = false, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping? packageSourceMapping = null) { CancellationToken cancellationToken = CancellationToken.None; @@ -346,30 +341,26 @@ private static bool PackageIsInAllowList(IEnumerable files) return true; } - private IEnumerable LoadOverrideSources(PackageSourceLocation packageSourceLocation = null) + private IEnumerable LoadOverrideSources(PackageSourceLocation? packageSourceLocation = null) { - foreach (string source in packageSourceLocation?.SourceFeedOverrides) - { - if (string.IsNullOrWhiteSpace(source)) - { - continue; - } + var sources = packageSourceLocation?.SourceFeedOverrides + .Where(source => !string.IsNullOrWhiteSpace(source)) + .Select(source => new PackageSource(source)); - PackageSource packageSource = new(source); - if (packageSource.TrySourceAsUri == null) + foreach (PackageSource source in sources ?? []) + { + if (source.TrySourceAsUri == null) { _verboseLogger.LogWarning(string.Format( CliStrings.FailedToLoadNuGetSource, - source)); + source.Source)); continue; } - - yield return packageSource; + yield return source; } - } - private List LoadDefaultSources(PackageId packageId, PackageSourceLocation packageSourceLocation = null, PackageSourceMapping packageSourceMapping = null) + private List LoadDefaultSources(PackageId packageId, PackageSourceLocation? packageSourceLocation = null, PackageSourceMapping? packageSourceMapping = null) { List defaultSources = []; string currentDirectory = _currentWorkingDirectory ?? Directory.GetCurrentDirectory(); @@ -411,7 +402,7 @@ private List LoadDefaultSources(PackageId packageId, PackageSourc if (packageSourceLocation?.AdditionalSourceFeed?.Any() ?? false) { - foreach (string source in packageSourceLocation?.AdditionalSourceFeed) + foreach (string source in packageSourceLocation?.AdditionalSourceFeed ?? []) { if (string.IsNullOrWhiteSpace(source)) { @@ -439,7 +430,7 @@ private List LoadDefaultSources(PackageId packageId, PackageSourc return defaultSources; } - public IEnumerable LoadNuGetSources(PackageId packageId, PackageSourceLocation packageSourceLocation = null, PackageSourceMapping packageSourceMapping = null) + public IEnumerable LoadNuGetSources(PackageId packageId, PackageSourceLocation? packageSourceLocation = null, PackageSourceMapping? packageSourceMapping = null) { var sources = (packageSourceLocation?.SourceFeedOverrides.Any() ?? false) ? LoadOverrideSources(packageSourceLocation) : @@ -605,9 +596,10 @@ await Task.WhenAll( return latestVersions.Take(numberOfResults); } - public async Task GetBestPackageVersionAsync(PackageId packageId, + public async Task GetBestPackageVersionAsync( + PackageId packageId, VersionRange versionRange, - PackageSourceLocation packageSourceLocation = null) + PackageSourceLocation? packageSourceLocation = null) { if (versionRange.MinVersion != null && versionRange.MaxVersion != null && versionRange.MinVersion == versionRange.MaxVersion) { @@ -619,9 +611,10 @@ public async Task GetBestPackageVersionAsync(PackageId packageId, .version; } - public async Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync(PackageId packageId, + public async Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync( + PackageId packageId, VersionRange versionRange, - PackageSourceLocation packageSourceLocation = null) + PackageSourceLocation? packageSourceLocation = null) { CancellationToken cancellationToken = CancellationToken.None; IPackageSearchMetadata packageMetadata; @@ -663,7 +656,7 @@ bool TryGetPackageMetadata( } atLeastOneSourceValid = true; - IPackageSearchMetadata matchedVersion = + IPackageSearchMetadata? matchedVersion = sourceAndFoundPackages.foundPackages.FirstOrDefault(package => package.Identity.Version == packageVersion); if (matchedVersion != null) @@ -755,13 +748,13 @@ bool TryGetPackageMetadata( } public async Task GetLatestPackageVersion(PackageId packageId, - PackageSourceLocation packageSourceLocation = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { return (await GetLatestPackageVersions(packageId, numberOfResults: 1, packageSourceLocation, includePreview)).First(); } - public async Task> GetLatestPackageVersions(PackageId packageId, int numberOfResults, PackageSourceLocation packageSourceLocation = null, bool includePreview = false) + public async Task> GetLatestPackageVersions(PackageId packageId, int numberOfResults, PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { CancellationToken cancellationToken = CancellationToken.None; IEnumerable packagesSources = LoadNuGetSources(packageId, packageSourceLocation); @@ -771,7 +764,7 @@ public async Task> GetLatestPackageVersions(PackageId result.Item2.Identity.Version); } - public async Task> GetPackageIdsAsync(string idStem, bool allowPrerelease, PackageSourceLocation packageSourceLocation = null, CancellationToken cancellationToken = default) + public async Task> GetPackageIdsAsync(string idStem, bool allowPrerelease, PackageSourceLocation? packageSourceLocation = null, CancellationToken cancellationToken = default) { // grab allowed sources for the package in question PackageId packageId = new(idStem); @@ -786,7 +779,7 @@ public async Task> GetPackageIdsAsync(string idStem, bool al return packageIdLists.SelectMany(v => v).Distinct().OrderDescending(); } - public async Task> GetPackageVersionsAsync(PackageId packageId, string versionPrefix = null, bool allowPrerelease = false, PackageSourceLocation packageSourceLocation = null, CancellationToken cancellationToken = default) + public async Task> GetPackageVersionsAsync(PackageId packageId, string? versionPrefix = null, bool allowPrerelease = false, PackageSourceLocation? packageSourceLocation = null, CancellationToken cancellationToken = default) { // grab allowed sources for the package in question IEnumerable packagesSources = LoadNuGetSources(packageId, packageSourceLocation); @@ -813,7 +806,7 @@ private async Task> GetAutocompleteAsync(Packa // only exposed for testing internal static TimeSpan CliCompletionsTimeout { get; set; } = TimeSpan.FromMilliseconds(500); - private async Task> GetPackageVersionsForSource(AutoCompleteResource autocomplete, PackageId packageId, string versionPrefix, bool allowPrerelease, CancellationToken cancellationToken) + private async Task> GetPackageVersionsForSource(AutoCompleteResource autocomplete, PackageId packageId, string? versionPrefix, bool allowPrerelease, CancellationToken cancellationToken) { try { @@ -853,7 +846,7 @@ private static async Task> GetPackageIdsForSource(AutoComple private SourceRepository GetSourceRepository(PackageSource source) { - if (!_sourceRepositories.TryGetValue(source, out SourceRepository value)) + if (!_sourceRepositories.TryGetValue(source, out SourceRepository? value)) { value = Repository.Factory.GetCoreV3(source); _sourceRepositories.AddOrUpdate(source, _ => value, (_, _) => value); diff --git a/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs b/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs index 8f8ceb39ad62..2ccef2997acf 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs @@ -7,7 +7,7 @@ using System.Xml; using System.Xml.Serialization; -namespace Microsoft.DotNet.Cli.NugetPackageDownloader; +namespace Microsoft.DotNet.Cli.NuGetPackageDownloader; [Serializable] [DesignerCategory("code")] diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index fa3fc09973f0..3ae8a49312d5 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -16,6 +16,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.Configurer; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Frameworks; using CommandResult = System.CommandLine.Parsing.CommandResult; @@ -177,17 +178,19 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) { PerformanceLogEventSource.Log.FirstTimeConfigurationStart(); - var environmentProvider = new EnvironmentProvider(); + // Initialize the new configuration-based environment provider + var configuration = DotNetConfigurationFactory.Create(); - bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true); - bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); - bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true); - bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false); - bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK, + // Use typed configuration directly instead of environment variable calls + bool generateAspNetCertificate = configuration.FirstTimeUse.GenerateAspNetCertificate; + bool telemetryOptout = configuration.CliUserExperience.TelemetryOptOut; + bool addGlobalToolsToPath = configuration.FirstTimeUse.AddGlobalToolsToPath; + bool nologo = configuration.CliUserExperience.NoLogo; + bool skipWorkloadIntegrityCheck = configuration.Workload.SkipIntegrityCheck || // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false. - defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment()); + new CIEnvironmentDetectorForTelemetry().IsCIEnvironment(); - ReportDotnetHomeUsage(environmentProvider); + ReportDotnetHomeUsage(configuration); var isDotnetBeingInvokedFromNativeInstaller = false; if (parseResult.CommandResult.Command.Name.Equals(Parser.InstallSuccessCommand.Name)) @@ -217,7 +220,7 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) toolPathSentinel, isDotnetBeingInvokedFromNativeInstaller, dotnetFirstRunConfiguration, - environmentProvider, + new EnvironmentProvider(), performanceData, skipFirstTimeUseCheck: getStarOptionPassed); PerformanceLogEventSource.Log.FirstTimeConfigurationStop(); @@ -349,10 +352,9 @@ private static int AdjustExitCode(ParseResult parseResult, int exitCode) return exitCode; } - private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) + private static void ReportDotnetHomeUsage(DotNetCliConfiguration config) { - var home = provider.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName); - if (string.IsNullOrEmpty(home)) + if (string.IsNullOrEmpty(config.Development.CliHome)) { return; } @@ -360,7 +362,7 @@ private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) Reporter.Verbose.WriteLine( string.Format( LocalizableStrings.DotnetCliHomeUsed, - home, + config.Development.CliHome, CliFolderPathCalculator.DotnetHomeVariableName)); } diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index ee9ce79b1593..18f3e118cde5 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.NET.Build.Tasks; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -31,6 +32,7 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co private readonly ParseResult _parseResult; private readonly string _propertyToCheck; + private readonly DotNetCliConfiguration _configurationService; DependentCommandOptions _options; private readonly IEnumerable _slnOrProjectArgs; @@ -45,9 +47,10 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co public ReleasePropertyProjectLocator( ParseResult parseResult, string propertyToCheck, - DependentCommandOptions commandOptions + DependentCommandOptions commandOptions, + DotNetCliConfiguration? configurationService = null ) - => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs); + => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs, _configurationService) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs, configurationService ?? DotNetConfigurationFactory.Create()); /// /// Return dotnet CLI command-line parameters (or an empty list) to change configuration based on ... @@ -58,7 +61,7 @@ DependentCommandOptions commandOptions { // Setup Debug.Assert(_propertyToCheck == MSBuildPropertyNames.PUBLISH_RELEASE || _propertyToCheck == MSBuildPropertyNames.PACK_RELEASE, "Only PackRelease or PublishRelease are currently expected."); - if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE), "true", StringComparison.OrdinalIgnoreCase)) + if (_configurationService.Build.DisablePublishAndPackRelease) { return null; } @@ -160,7 +163,7 @@ DependentCommandOptions commandOptions HashSet configValues = []; object projectDataLock = new(); - if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS), "true", StringComparison.OrdinalIgnoreCase)) + if (_configurationService.Build.LazyPublishAndPackReleaseForSolutions) { // Evaluate only one project for speed if this environment variable is used. Will break more customers if enabled (adding 8.0 project to SLN with other project TFMs with no Publish or PackRelease.) return GetSingleProjectFromSolution(sln, slnFullPath, globalProps); diff --git a/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs b/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs index 935f9e59098b..c6b22ffcb48b 100644 --- a/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs +++ b/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Frameworks; -using NuGet.Versioning; namespace Microsoft.DotNet.Cli.ShellShim; @@ -21,7 +18,7 @@ internal class ShellShimTemplateFinder( private readonly INuGetPackageDownloader _nugetPackageDownloader = nugetPackageDownloader; private readonly PackageSourceLocation _packageSourceLocation = packageSourceLocation; - public async Task ResolveAppHostSourceDirectoryAsync(string archOption, NuGetFramework targetFramework, Architecture arch) + public async Task ResolveAppHostSourceDirectoryAsync(string? archOption, NuGetFramework? targetFramework, Architecture arch) { string rid; var validRids = new string[] { "win-x64", "win-arm64", "osx-x64", "osx-arm64" }; @@ -51,8 +48,7 @@ public async Task ResolveAppHostSourceDirectoryAsync(string archOption, } var packageId = new PackageId($"microsoft.netcore.app.host.{rid}"); - NuGetVersion packageVersion = null; - var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation: _packageSourceLocation); + var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(packageId, null, packageSourceLocation: _packageSourceLocation); _ = await _nugetPackageDownloader.ExtractPackageAsync(packagePath, _tempDir); return Path.Combine(_tempDir.Value, "runtimes", rid, "native"); diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index d255b33e93a6..491571974ffa 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs new file mode 100644 index 000000000000..2b1450e79495 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Providers; + +namespace Microsoft.Extensions.Configuration.DotnetCli; + +/// +/// Factory for creating .NET CLI configuration instances. +/// +public static class DotNetConfiguration +{ + /// + /// Creates a complete configuration instance with all providers. + /// + /// The working directory to search for configuration files. Defaults to current directory. + /// A configured IConfiguration instance. + public static IConfiguration Create(string? workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Priority order (last wins): + // 1. dotnet.config (if it exists) - future enhancement + // 2. global.json (custom provider with key mapping) + // 3. Environment variables with DOTNET_ prefix (with key mapping) + // 4. Command line arguments (handled separately) + + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Add dotnet.config with custom key mapping + builder.Add(new DotNetConfigurationSource(workingDirectory)); + + // Add global.json with a custom configuration provider that maps keys + builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // Add DOTNET_ prefixed environment variables with key mapping + builder.Add(new DotNetEnvironmentConfigurationSource()); + + return builder.Build(); + } + + /// + /// Creates a strongly-typed configuration service with all providers. + /// + /// The working directory to search for configuration files. Defaults to current directory. + /// A strongly-typed configuration service. + public static DotNetCliConfiguration CreateTyped(string? workingDirectory = null) + { + var configuration = Create(workingDirectory); + return new DotNetCliConfiguration(configuration); + } + + /// + /// Creates a minimal configuration service that only loads environment variables. + /// This is the fastest option for scenarios that don't need file-based configuration. + /// + /// The working directory (unused for minimal configuration). + /// A minimal strongly-typed configuration service. + public static DotNetCliConfiguration CreateMinimal(string? workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Only add environment variables for minimal overhead + builder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = builder.Build(); + return new DotNetCliConfiguration(configuration); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs new file mode 100644 index 000000000000..eb42e9a99bcf --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Providers; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Configuration.DotnetCli; + +/// +/// Factory for creating and configuring the .NET CLI configuration service. +/// This is the main entry point for the unified configuration system. +/// +public static class DotNetConfigurationFactory +{ + // Cache for the default configuration service instance + private static readonly Lazy _defaultInstance = new(() => CreateInternal(null), LazyThreadSafetyMode.ExecutionAndPublication); + + // Cache for configuration services by working directory + private static readonly ConcurrentDictionary> _instancesByDirectory = new(); + + /// + /// Creates and configures the .NET CLI configuration service with default providers. + /// This method follows the layered configuration approach: environment variables override global.json, + /// and configuration is loaded lazily for performance. + /// Results are cached to avoid repeated expensive configuration building. + /// + /// The working directory to search for global.json files. Defaults to current directory. + /// A configured DotNetCliConfiguration instance + public static DotNetCliConfiguration Create(string? workingDirectory = null) + { + if (workingDirectory == null) + { + // Use the default cached instance for null working directory + return _defaultInstance.Value; + } + + // Normalize the working directory path for consistent caching + var normalizedPath = Path.GetFullPath(workingDirectory); + + // Get or create a cached instance for this specific working directory + var lazyInstance = _instancesByDirectory.GetOrAdd(normalizedPath, + path => new Lazy(() => CreateInternal(path), LazyThreadSafetyMode.ExecutionAndPublication)); + + return lazyInstance.Value; + } + + /// + /// Internal method that performs the actual configuration creation without caching. + /// + /// The working directory to search for configuration files. + /// A configured DotNetConfigurationService instance + private static DotNetCliConfiguration CreateInternal(string? workingDirectory) + { + workingDirectory ??= Directory.GetCurrentDirectory(); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.SetBasePath(workingDirectory); + + // Configuration sources are added in reverse precedence order + // Last added has highest precedence + + // 1. dotnet.config (custom provider with key mapping) - lowest precedence + configurationBuilder.Add(new DotNetConfigurationSource(workingDirectory)); + + // 2. global.json (custom provider with key mapping) - medium precedence + configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // 3. Environment variables (custom provider with key mapping) - highest precedence + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = configurationBuilder.Build(); + + return new DotNetCliConfiguration(configuration); + } + + /// + /// Creates a minimal configuration service with only environment variables. + /// This is useful for scenarios where global.json lookup is not needed or desirable, + /// such as in performance-critical paths or testing scenarios. + /// + /// A minimal DotNetConfigurationService instance + public static DotNetCliConfiguration CreateMinimal() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + var configuration = configurationBuilder.Build(); + return new DotNetCliConfiguration(configuration); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs new file mode 100644 index 000000000000..0cec13f1f232 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +namespace Microsoft.Extensions.Configuration.DotnetCli; + +/// +/// Strongly-typed configuration for .NET CLI with lazy initialization. +/// +public class DotNetCliConfiguration +{ + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + private readonly Lazy _nuget; + private readonly Lazy _test; + + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + public NuGetConfiguration NuGet => _nuget.Value; + public TestConfiguration Test => _test.Value; + + public DotNetCliConfiguration(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + _configuration.GetSection("CliUserExperience").Get() ?? new()); + _runtimeHost = new Lazy(() => + _configuration.GetSection("RuntimeHost").Get() ?? new()); + _build = new Lazy(() => + _configuration.GetSection("Build").Get() ?? new()); + _sdkResolver = new Lazy(() => + _configuration.GetSection("SdkResolver").Get() ?? new()); + _workload = new Lazy(() => + _configuration.GetSection("Workload").Get() ?? new()); + _firstTimeUse = new Lazy(() => + _configuration.GetSection("FirstTimeUse").Get() ?? new()); + _development = new Lazy(() => + _configuration.GetSection("Development").Get() ?? new()); + _tool = new Lazy(() => + _configuration.GetSection("Tool").Get() ?? new()); + _nuget = new Lazy(() => + _configuration.GetSection("NuGet").Get() ?? new()); + _test = new Lazy(() => + _configuration.GetSection("Test").Get() ?? new()); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj new file mode 100644 index 000000000000..1e9c02fa0fd4 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj @@ -0,0 +1,22 @@ + + + + $(SdkTargetFramework) + 12.0 + enable + true + + + + + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs new file mode 100644 index 000000000000..32761bb5b8ac --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control build system behavior. +/// +public sealed class BuildConfiguration +{ + /// + /// Gets or sets whether to run MSBuild out of process. + /// Mapped from DOTNET_CLI_RUN_MSBUILD_OUTOFPROC environment variable. + /// + public bool RunMSBuildOutOfProc { get; set; } = false; + + /// + /// Gets or sets whether to use the MSBuild server for builds. + /// Mapped from DOTNET_CLI_USE_MSBUILD_SERVER environment variable. + /// + public bool UseMSBuildServer { get; set; } = false; + + /// + /// Gets or sets the configuration for the MSBuild terminal logger. + /// Mapped from DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER environment variable. + /// + public string? ConfigureMSBuildTerminalLogger { get; set; } + + /// + /// Gets or sets whether to disable publish and pack release configuration. + /// Mapped from DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE environment variable. + /// + public bool DisablePublishAndPackRelease { get; set; } = false; + + /// + /// Gets or sets whether to enable lazy publish and pack release for solutions. + /// Mapped from DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS environment variable. + /// + public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs new file mode 100644 index 000000000000..46618744d900 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs @@ -0,0 +1,42 @@ +// 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; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control the CLI's user interface and interaction behavior. +/// +public sealed class CliUserExperienceConfiguration +{ + /// + /// Gets or sets whether telemetry collection is disabled. + /// Mapped from DOTNET_CLI_TELEMETRY_OPTOUT environment variable. + /// + public bool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; + + /// + /// Gets or sets whether to suppress the .NET logo on startup. + /// Mapped from DOTNET_NOLOGO environment variable. + /// + public bool NoLogo { get; set; } = false; + + /// + /// Gets or sets whether to force UTF-8 encoding for console output. + /// Mapped from DOTNET_CLI_FORCE_UTF8_ENCODING environment variable. + /// + public bool ForceUtf8Encoding { get; set; } = false; + + /// + /// Gets or sets the UI language for the CLI. + /// Mapped from DOTNET_CLI_UI_LANGUAGE environment variable. + /// + public string? UILanguage { get; set; } + + /// + /// Gets or sets the telemetry profile for data collection. + /// Mapped from DOTNET_CLI_TELEMETRY_PROFILE environment variable. + /// + public string? TelemetryProfile { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs new file mode 100644 index 000000000000..ab442308c9d3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control development tools and debugging features. +/// +public sealed class DevelopmentConfiguration +{ + /// + /// Gets or sets whether performance logging is enabled. + /// Mapped from DOTNET_CLI_PERF_LOG environment variable. + /// + public bool PerfLogEnabled { get; set; } = false; + + /// + /// Gets or sets the number of performance log entries to collect. + /// Mapped from DOTNET_PERF_LOG_COUNT environment variable. + /// + public string? PerfLogCount { get; set; } + + /// + /// Gets or sets the CLI home directory for configuration and data. + /// Mapped from DOTNET_CLI_HOME environment variable. + /// + public string? CliHome { get; set; } + + /// + /// Gets or sets whether to enable verbose context logging. + /// Mapped from DOTNET_CLI_CONTEXT_VERBOSE environment variable. + /// + public bool ContextVerbose { get; set; } = false; + + /// + /// Gets or sets whether to allow targeting pack caching. + /// Mapped from DOTNETSDK_ALLOW_TARGETING_PACK_CACHING environment variable. + /// + public bool AllowTargetingPackCaching { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs new file mode 100644 index 000000000000..54d813f3f202 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.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.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control first-time user experience setup. +/// +public sealed class FirstTimeUseConfiguration +{ + /// + /// Gets or sets whether to generate ASP.NET Core HTTPS development certificates. + /// Mapped from DOTNET_GENERATE_ASPNET_CERTIFICATE environment variable. + /// + public bool GenerateAspNetCertificate { get; set; } = true; + + /// + /// Gets or sets whether to add global tools to the PATH. + /// Mapped from DOTNET_ADD_GLOBAL_TOOLS_TO_PATH environment variable. + /// + public bool AddGlobalToolsToPath { get; set; } = true; + + /// + /// Gets or sets whether to skip the first-time experience setup. + /// Mapped from DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable. + /// + public bool SkipFirstTimeExperience { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs new file mode 100644 index 000000000000..d49db7afbfa3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs @@ -0,0 +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.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control NuGet package management behavior. +/// +public sealed class NuGetConfiguration +{ + /// + /// Gets or sets whether NuGet signature verification is enabled. + /// Mapped from DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable. + /// Defaults to true on Windows and Linux, false elsewhere. + /// + public bool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs new file mode 100644 index 000000000000..546f63e5f987 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control .NET runtime host behavior. +/// +public sealed class RuntimeHostConfiguration +{ + /// + /// Gets or sets the path to the .NET host executable. + /// Mapped from DOTNET_HOST_PATH environment variable. + /// + public string? HostPath { get; set; } + + /// + /// Gets or sets whether to enable multilevel lookup for shared frameworks. + /// Mapped from DOTNET_MULTILEVEL_LOOKUP environment variable. + /// + public bool MultilevelLookup { get; set; } = false; + + /// + /// Gets or sets the roll-forward policy for framework version selection. + /// Mapped from DOTNET_ROLL_FORWARD environment variable. + /// + public string? RollForward { get; set; } + + /// + /// Gets or sets the roll-forward policy when no candidate framework is found. + /// Mapped from DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX environment variable. + /// + public string? RollForwardOnNoCandidateFx { get; set; } + + /// + /// Gets or sets the root directory for .NET installations. + /// Mapped from DOTNET_ROOT environment variable. + /// + public string? Root { get; set; } + + /// + /// Gets or sets the root directory for x86 .NET installations. + /// Mapped from DOTNET_ROOT(x86) environment variable. + /// + public string? RootX86 { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs new file mode 100644 index 000000000000..2769c5038607 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control SDK resolution and discovery. +/// +public sealed class SdkResolverConfiguration +{ + /// + /// Gets or sets whether to enable SDK resolver logging. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG environment variable. + /// + public bool EnableLog { get; set; } = false; + + /// + /// Gets or sets the directory containing SDKs. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR environment variable. + /// + public string? SdksDirectory { get; set; } + + /// + /// Gets or sets the SDK version to use. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER environment variable. + /// + public string? SdksVersion { get; set; } + + /// + /// Gets or sets the CLI directory for SDK resolution. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR environment variable. + /// + public string? CliDirectory { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs new file mode 100644 index 000000000000..14d1b866a9e4 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs @@ -0,0 +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.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control test runner behavior. +/// +public sealed class TestConfiguration +{ + /// + /// Gets or sets the test runner name to use. + /// Mapped from dotnet.config file: [dotnet.test.runner] name=VALUE + /// Defaults to "VSTest" if not specified. + /// + public string RunnerName { get; set; } = "VSTest"; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs new file mode 100644 index 000000000000..89c4fd4d6fc3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control global tools behavior. +/// +public sealed class ToolConfiguration +{ + /// + /// Gets or sets whether to allow tool manifests in the repository root. + /// Mapped from DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT environment variable. + /// + public bool AllowManifestInRoot { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs new file mode 100644 index 000000000000..85e988fc1406 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control workload management and behavior. +/// +public sealed class WorkloadConfiguration +{ + /// + /// Gets or sets whether to disable workload update notifications. + /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE environment variable. + /// + public bool UpdateNotifyDisable { get; set; } = false; + + /// + /// Gets or sets the interval in hours between workload update notifications. + /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS environment variable. + /// + public int UpdateNotifyIntervalHours { get; set; } = 24; // Default to check once per day + + /// + /// Gets or sets whether to disable workload pack groups. + /// Mapped from DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS environment variable. + /// + public bool DisablePackGroups { get; set; } = false; + + /// + /// Gets or sets whether to skip workload integrity checks. + /// Mapped from DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK environment variable. + /// + public bool SkipIntegrityCheck { get; set; } = false; + + /// + /// Gets or sets the manifest root directories for workloads. + /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_ROOTS environment variable. + /// + public string[]? ManifestRoots { get; set; } + + /// + /// Gets or sets the pack root directories for workloads. + /// Mapped from DOTNETSDK_WORKLOAD_PACK_ROOTS environment variable. + /// + public string[]? PackRoots { get; set; } + + /// + /// Gets or sets whether to ignore default manifest roots. + /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS environment variable. + /// + public bool ManifestIgnoreDefaultRoots { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs new file mode 100644 index 000000000000..b855eee64b9d --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Ini; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider for dotnet.config INI files with key mapping. +/// Maps dotnet.config keys to the expected configuration structure. +/// +public class DotNetConfigurationProvider : IniConfigurationProvider +{ + private static readonly Dictionary KeyMappings = new(StringComparer.OrdinalIgnoreCase) + { + // Map INI section:key to expected configuration path + ["dotnet.test.runner:name"] = "Test:RunnerName", + + // Future mappings can be added here for other dotnet.config settings + // ["dotnet.example.section:key"] = "ConfigSection:Property", + }; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration source. + public DotNetConfigurationProvider(DotNetConfigurationSource source) : base(source) + { + } + + /// + /// Loads configuration data with key transformation. + /// + public override void Load() + { + // Load the INI file normally first + base.Load(); + + // Transform keys according to our mapping + var transformedData = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in Data) + { + string key = kvp.Key; + string? value = kvp.Value; + + // Check if we have a mapping for this key + if (KeyMappings.TryGetValue(key, out string? mappedKey)) + { + transformedData[mappedKey] = value; + } + else + { + // Keep unmapped keys as-is + transformedData[key] = value; + } + } + + // Replace the data with transformed keys + Data = transformedData; + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs new file mode 100644 index 000000000000..084341cdefc4 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Ini; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for dotnet.config INI files with key mapping. +/// +public class DotNetConfigurationSource : IniConfigurationSource +{ + public DotNetConfigurationSource(string workingDirectory) + { + Path = System.IO.Path.Combine(workingDirectory, "dotnet.config"); + Optional = true; // Make it optional since dotnet.config may not exist + ResolveFileProvider(); + } + /// + /// Builds the configuration provider for dotnet.config files. + /// + /// The configuration builder. + /// The configuration provider. + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + EnsureDefaults(builder); + return new DotNetConfigurationProvider(this); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs new file mode 100644 index 000000000000..d21978065cfd --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider that maps DOTNET_ prefixed environment variables to canonical keys. +/// +public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider +{ + private static readonly Dictionary EnvironmentKeyMappings = new() + { + ["DOTNET_HOST_PATH"] = "RuntimeHost:HostPath", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "CliUserExperience:TelemetryOptOut", + ["DOTNET_NOLOGO"] = "CliUserExperience:NoLogo", + ["DOTNET_CLI_FORCE_UTF8_ENCODING"] = "CliUserExperience:ForceUtf8Encoding", + ["DOTNET_CLI_UI_LANGUAGE"] = "CliUserExperience:UILanguage", + ["DOTNET_CLI_TELEMETRY_PROFILE"] = "CliUserExperience:TelemetryProfile", + ["DOTNET_CLI_PERF_LOG"] = "Development:PerfLogEnabled", + ["DOTNET_PERF_LOG_COUNT"] = "Development:PerfLogCount", + ["DOTNET_CLI_HOME"] = "Development:CliHome", + ["DOTNET_CLI_CONTEXT_VERBOSE"] = "Development:ContextVerbose", + ["DOTNET_MULTILEVEL_LOOKUP"] = "RuntimeHost:MultilevelLookup", + ["DOTNET_ROLL_FORWARD"] = "RuntimeHost:RollForward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "RuntimeHost:RollForwardOnNoCandidateFx", + ["DOTNET_ROOT"] = "RuntimeHost:Root", + ["DOTNET_ROOT(x86)"] = "RuntimeHost:RootX86", + ["DOTNET_CLI_RUN_MSBUILD_OUTOFPROC"] = "Build:RunMSBuildOutOfProc", + ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "Build:UseMSBuildServer", + ["DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER"] = "Build:ConfigureMSBuildTerminalLogger", + ["DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE"] = "Build:DisablePublishAndPackRelease", + ["DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS"] = "Build:LazyPublishAndPackReleaseForSolutions", + ["DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG"] = "SdkResolver:EnableLog", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"] = "SdkResolver:SdksDirectory", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"] = "SdkResolver:SdksVersion", + ["DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"] = "SdkResolver:CliDirectory", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE"] = "Workload:UpdateNotifyDisable", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS"] = "Workload:UpdateNotifyIntervalHours", + ["DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS"] = "Workload:DisablePackGroups", + ["DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK"] = "Workload:SkipIntegrityCheck", + ["DOTNETSDK_WORKLOAD_MANIFEST_ROOTS"] = "Workload:ManifestRoots", + ["DOTNETSDK_WORKLOAD_PACK_ROOTS"] = "Workload:PackRoots", + ["DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS"] = "Workload:ManifestIgnoreDefaultRoots", + ["DOTNETSDK_ALLOW_TARGETING_PACK_CACHING"] = "Development:AllowTargetingPackCaching", + ["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "FirstTimeUse:GenerateAspNetCertificate", + ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "FirstTimeUse:AddGlobalToolsToPath", + ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "FirstTimeUse:SkipFirstTimeExperience", + ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", + ["DOTNET_NUGET_SIGNATURE_VERIFICATION"] = "NuGet:SignatureVerificationEnabled", + }; + private static readonly HashSet ConfigKeysToTreatAsBooleans = [ + "CliUserExperience:TelemetryOptOut", + "CliUserExperience:NoLogo", + "CliUserExperience:ForceUtf8Encoding", + "Development:PerfLogEnabled", + "Development:ContextVerbose", + "Development:AllowTargetingPackCaching", + "RuntimeHost:MultilevelLookup", + "Build:RunMSBuildOutOfProc", + "Build:UseMSBuildServer", + "Build:DisablePublishAndPackRelease", + "Build:LazyPublishAndPackReleaseForSolutions", + "SdkResolver:EnableLog", + "Workload:UpdateNotifyDisable", + "Workload:DisablePackGroups", + "Workload:SkipIntegrityCheck", + "Workload:ManifestIgnoreDefaultRoots", + "FirstTimeUse:GenerateAspNetCertificate", + "FirstTimeUse:AddGlobalToolsToPath", + "FirstTimeUse:SkipFirstTimeExperience", + "Tool:AllowManifestInRoot", + "NuGet:SignatureVerificationEnabled" + ]; + + public override void Load() + { + Data.Clear(); + + foreach (var mapping in EnvironmentKeyMappings) + { + var value = Environment.GetEnvironmentVariable(mapping.Key); + if (!string.IsNullOrEmpty(value)) + { + Data[mapping.Value] = CoerceValueIfNecessary(mapping.Value, value); + } + } + + // Handle array-type environment variables (semicolon-separated) + HandleArrayEnvironmentVariable("DOTNETSDK_WORKLOAD_MANIFEST_ROOTS", "Workload:ManifestRoots"); + HandleArrayEnvironmentVariable("DOTNETSDK_WORKLOAD_PACK_ROOTS", "Workload:PackRoots"); + } + + private string CoerceValueIfNecessary(string configKey, string envValue) => ConfigKeysToTreatAsBooleans.Contains(configKey) + ? GetFlexibleBool(envValue).ToString() + : envValue; + + private bool GetFlexibleBool(string envValue) => + bool.TryParse(envValue, out var boolValue) + ? boolValue + : int.TryParse(envValue, out var intValue) ? intValue > 0 + : envValue switch + { + "yes" or "y" => true, + "no" or "n" => false, + _ => throw new FormatException($"Invalid boolean value: {envValue}") + }; + + private void HandleArrayEnvironmentVariable(string envVar, string configKey) + { + var value = Environment.GetEnvironmentVariable(envVar); + if (!string.IsNullOrEmpty(value)) + { + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < parts.Length; i++) + { + Data[$"{configKey}:{i}"] = parts[i]; + } + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs new file mode 100644 index 000000000000..8e5d83464df3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for DOTNET_ prefixed environment variables. +/// +public class DotNetEnvironmentConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new DotNetEnvironmentConfigurationProvider(); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs new file mode 100644 index 000000000000..ec05de8794d1 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Json; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider that reads global.json files and maps keys to the canonical format. +/// +public class GlobalJsonConfigurationProvider : JsonConfigurationProvider +{ + private static readonly Dictionary GlobalJsonKeyMappings = new() + { + ["sdk:version"] = "sdk:version", + ["sdk:rollForward"] = "sdk:rollforward", + ["sdk:allowPrerelease"] = "sdk:allowprerelease", + ["msbuild-sdks"] = "msbuild:sdks", + // Add more mappings as the global.json schema evolves + }; + + public GlobalJsonConfigurationProvider(GlobalJsonConfigurationSource source) : base(source) + { + } + + public override void Load() + { + base.Load(); + // Transform keys according to our mapping + var transformedData = new Dictionary(Data.Count, StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in Data) + { + string key = kvp.Key; + string? value = kvp.Value; + var mappedKey = MapGlobalJsonKey(key); + transformedData[mappedKey] = value; + } + + // Replace the data with transformed keys + Data = transformedData; + } + + private string MapGlobalJsonKey(string rawKey) + { + // Check for exact mapping first + if (GlobalJsonKeyMappings.TryGetValue(rawKey, out var mapped)) + return mapped; + + // For msbuild-sdks, convert to msbuild:sdks:packagename format + if (rawKey.StartsWith("msbuild-sdks:")) + return rawKey.Replace("msbuild-sdks:", "msbuild:sdks:"); + + // Default: convert to lowercase and normalize separators + return rawKey.ToLowerInvariant().Replace("-", ":"); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs new file mode 100644 index 000000000000..3b6983a3812a --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Json; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for global.json files. +/// +public class GlobalJsonConfigurationSource : JsonConfigurationSource +{ + + public GlobalJsonConfigurationSource(string workingDirectory) + { + Path = System.IO.Path.Combine(workingDirectory, "global.json"); + Optional = true; + ResolveFileProvider(); + } + + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + EnsureDefaults(builder); + return new GlobalJsonConfigurationProvider(this); + } +} diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs index 4d76a2484948..7cabcff704e2 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; @@ -12,8 +11,11 @@ namespace Microsoft.DotNet.PackageInstall.Tests { public class NuGetPackageInstallerExtractTests : SdkTest { + private readonly DotNetCliConfiguration _config; + public NuGetPackageInstallerExtractTests(ITestOutputHelper log) : base(log) { + _config = DotNetConfigurationFactory.CreateMinimal(); } [Fact] @@ -23,8 +25,10 @@ public async Task ItCanExtractNugetPackage() string packageVersion = ToolsetInfo.GetNewtonsoftJsonPackageVersion(); NuGetTestLogger logger = new(Log); NuGetPackageDownloader installer = - new(new DirectoryPath(Directory.GetCurrentDirectory()), null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + new(new DirectoryPath(Directory.GetCurrentDirectory()), + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); string packagePath = await installer.DownloadPackageAsync(new PackageId(packageId), new NuGetVersion(packageVersion)); string targetPath = Path.Combine(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), @@ -44,8 +48,9 @@ public void ItCanGetAllFilesNeedToSetExecutablePermission() NuGetPackageDownloader installer = new( new DirectoryPath(Directory.GetCurrentDirectory()), - null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); var allFiles = new List() { "/ExtractedPackage/Microsoft.Android.Sdk.Darwin.nuspec", @@ -67,7 +72,9 @@ public void GivenPackageNotInAllowListItCannotGetAllFilesNeedToSetExecutablePerm NuGetTestLogger logger = new(Log); NuGetPackageDownloader installer = new(new DirectoryPath(Directory.GetCurrentDirectory()), null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); var allFiles = new List() { "/ExtractedPackage/Not.In.Allow.List.nuspec", diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs index 8e0693efdad9..19a7bae8c60d 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.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. -#nullable disable - using System.Collections.ObjectModel; using System.Reflection; using System.Security.Cryptography; @@ -36,11 +34,18 @@ public NuGetPackageInstallerTests(ITestOutputHelper log) : base(log) _tempDirectory = GetUniqueTempProjectPathEachTest(); _logger = new NuGetTestLogger(); _installer = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), _logger, - restoreActionConfig: new RestoreActionConfig(NoCache: true), timer: () => ExponentialRetry.Timer(ExponentialRetry.TestingIntervals)); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); _toolInstaller = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), _logger, - restoreActionConfig: new RestoreActionConfig(NoCache: true), timer: () => ExponentialRetry.Timer(ExponentialRetry.TestingIntervals), shouldUsePackageSourceMapping: true); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true), + shouldUsePackageSourceMapping: true); } [Fact] @@ -76,8 +81,11 @@ await Assert.ThrowsAsync(() => public async Task GivenAFailedSourceAndIgnoreFailedSourcesItShouldNotThrowFatalProtocolException() { var installer = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, restoreActionConfig: new RestoreActionConfig(IgnoreFailedSources: true, NoCache: true)); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(IgnoreFailedSources: true, NoCache: true)); // should not throw FatalProtocolException // when there is at least one valid source, it should pass. @@ -145,7 +153,7 @@ public async Task GivenAConfigFileRootDirectoryPackageInstallSucceedsViaFindingN [Fact] public async Task GivenNoPackageVersionItCanInstallLatestVersionOfPackage() { - NuGetVersion packageVersion = null; + NuGetVersion? packageVersion = null; string packagePath = await _installer.DownloadPackageAsync( TestPackageId, packageVersion, @@ -232,9 +240,12 @@ public async Task WhenPassedIncludePreviewItInstallSucceeds() public async Task GivenANonSignedSdkItShouldPrintMessageOnce() { BufferedReporter bufferedReporter = new(); - NuGetPackageDownloader nuGetPackageDownloader = new(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, bufferedReporter, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + NuGetPackageDownloader nuGetPackageDownloader = new( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + reporter: bufferedReporter, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); await nuGetPackageDownloader.DownloadPackageAsync( TestPackageId, new NuGetVersion(TestPackageVersion), @@ -256,9 +267,13 @@ await nuGetPackageDownloader.DownloadPackageAsync( public async Task GivenANonSignedSdkItShouldNotPrintMessageInQuiet() { BufferedReporter bufferedReporter = new BufferedReporter(); - NuGetPackageDownloader nuGetPackageDownloader = new NuGetPackageDownloader(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, bufferedReporter, restoreActionConfig: new RestoreActionConfig(NoCache: true), verbosityOptions: VerbosityOptions.quiet); + NuGetPackageDownloader nuGetPackageDownloader = new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + reporter: bufferedReporter, + restoreActionConfig: new RestoreActionConfig(NoCache: true), + verbosityOptions: VerbosityOptions.quiet); await nuGetPackageDownloader.DownloadPackageAsync( TestPackageId, new NuGetVersion(TestPackageVersion), @@ -281,20 +296,22 @@ public void ItShouldHaveUpdateToDateCertificateSha() var samplePackage = DownloadSamplePackage(new PackageId("Microsoft.iOS.Ref")); var firstPartyNuGetPackageSigningVerifier = new FirstPartyNuGetPackageSigningVerifier(); - string shaFromPackage = GetShaFromSamplePackage(samplePackage); + string? shaFromPackage = GetShaFromSamplePackage(samplePackage); firstPartyNuGetPackageSigningVerifier._firstPartyCertificateThumbprints.Contains(shaFromPackage).Should() .BeTrue( $"Add {shaFromPackage} to the _firstPartyCertificateThumbprints of FirstPartyNuGetPackageSigningVerifier class. More info https://aka.ms/netsdkinternal-certificate-rotate"); } - private string DownloadSamplePackage(PackageId packageId) + private string DownloadSamplePackage(PackageId _) { - NuGetPackageDownloader nuGetPackageDownloader = new(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + NuGetPackageDownloader nuGetPackageDownloader = new( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); - return ExponentialRetry.ExecuteWithRetry( + return ExponentialRetry.ExecuteWithRetry( action: DownloadMostRecentSamplePackageFromPublicFeed, shouldStopRetry: result => result != null, maxRetryCount: 3, @@ -309,12 +326,13 @@ string DownloadMostRecentSamplePackageFromPublicFeed() return nuGetPackageDownloader.DownloadPackageAsync( new PackageId("Microsoft.iOS.Ref"), null, includePreview: true, packageSourceLocation: new PackageSourceLocation( - sourceFeedOverrides: new[] { "https://api.nuget.org/v3/index.json" })).GetAwaiter() + sourceFeedOverrides: ["https://api.nuget.org/v3/index.json"])).GetAwaiter() .GetResult(); } catch (Exception) { - return null; + Log.WriteLine("Failed to download sample package from public feed - this is catastrophic for the test, as it relies on the package being present."); + throw; } } } @@ -390,6 +408,6 @@ private static FilePath GenerateRandomNugetConfigFilePath() } private static string GetTestLocalFeedPath() => - Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed"); + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "TestAssetLocalNugetFeed"); } } diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs index 5d71dc5b4808..177d71e7a291 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.Cli.NugetPackageDownloader; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; namespace Microsoft.DotNet.PackageInstall.Tests { diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs index 50bdac18df00..a22fa2227d4d 100644 --- a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs +++ b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs @@ -23,7 +23,7 @@ internal class ToolPackageDownloaderMock2 : ToolPackageDownloaderBase List? _packages = null; - public ToolPackageDownloaderMock2(IToolPackageStore store, string runtimeJsonPathForTests, string currentWorkingDirectory, IFileSystem fileSystem) : base(store, runtimeJsonPathForTests, currentWorkingDirectory, fileSystem) + public ToolPackageDownloaderMock2(IToolPackageStore store, string runtimeJsonPathForTests, string? currentWorkingDirectory, IFileSystem fileSystem) : base(store, runtimeJsonPathForTests, currentWorkingDirectory, fileSystem) { } diff --git a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs index 43184db6b930..3f31984b8a16 100644 --- a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.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. -#nullable disable - using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli; @@ -79,8 +77,6 @@ public ToolInstallGlobalOrToolPathCommandTests(ITestOutputHelper log): base(log) fileSystem: _fileSystem); _createToolPackageStoreDownloaderUninstaller = (location, forwardArguments, workingDirectory) => (_toolPackageStore, _toolPackageStoreQuery, _toolPackageDownloader, _toolPackageUninstallerMock); - - _parseResult = Parser.Instance.Parse($"dotnet tool install -g {PackageId}"); } @@ -121,7 +117,8 @@ public void WhenDuplicateSourceIsPassedIgnore() nugetConfig: new FilePath(Path.Combine(testAsset.Path, "NuGet.config")), rootConfigDirectory: new DirectoryPath(testAsset.Path), additionalSourceFeeds: [duplicateSource]); - var nuGetPackageDownloader = new NuGetPackageDownloader(new DirectoryPath(testAsset.Path)); + var nuGetPackageDownloader = new NuGetPackageDownloader( + packageInstallDir: new DirectoryPath(testAsset.Path)); var sources = nuGetPackageDownloader.LoadNuGetSources(new Cli.ToolPackage.PackageId(PackageId), packageSourceLocation); // There should only be one source @@ -148,7 +145,7 @@ public void WhenRunWithPackageIdItShouldCreateValidShim() // It is hard to simulate shell behavior. Only Assert shim can point to executable dll _fileSystem.File.Exists(ExpectedCommandPath()).Should().BeTrue(); var deserializedFakeShim = JsonSerializer.Deserialize( - _fileSystem.File.ReadAllText(ExpectedCommandPath())); + _fileSystem.File.ReadAllText(ExpectedCommandPath()))!; _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); } @@ -215,7 +212,7 @@ public void WhenRunWithPackageIdWithSourceItShouldCreateValidShim() .Should().BeTrue(); var deserializedFakeShim = JsonSerializer.Deserialize( - _fileSystem.File.ReadAllText(ExpectedCommandPath())); + _fileSystem.File.ReadAllText(ExpectedCommandPath()))!; _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); } @@ -558,7 +555,7 @@ public void WhenInstallWithLowerVersionWithAllowDowngradeOptionItShouldDowngrade public void WhenInstallWithLowerVersionItShouldFail() { AddLowerToolPackageVersionToFeed(); - + ParseResult result = Parser.Instance.Parse($"dotnet tool install -g {PackageId} --version {PackageVersion}"); var toolInstallGlobalOrToolPathCommand = new ToolInstallGlobalOrToolPathCommand( @@ -640,7 +637,10 @@ public void WhenRunWithValidVersionItShouldInterpretAsNuGetExactVersion(string v return (toolPackageStore, toolPackageStore, toolPackageDownloader, toolPackageUninstaller); }, createShellShimRepository: _createShellShimRepository, - nugetPackageDownloader: new NuGetPackageDownloader(new DirectoryPath(PathUtilities.CreateTempSubdirectory()), verifySignatures: false, currentWorkingDirectory: testDir), + nugetPackageDownloader: new NuGetPackageDownloader( + packageInstallDir: new DirectoryPath(PathUtilities.CreateTempSubdirectory()), + verifySignatures: false, + currentWorkingDirectory: testDir), currentWorkingDirectory: testDir, verifySignatures: false); @@ -753,7 +753,7 @@ private void AddLowerToolPackageVersionToFeed() }); } - + private void AddHigherToolPackageVersionToFeed() { _toolPackageDownloader.AddMockPackage(new MockFeedPackage