diff --git a/src/Cli/dotnet/Commands/Tool/Common/ToolManifestFinderExtensions.cs b/src/Cli/dotnet/Commands/Tool/Common/ToolManifestFinderExtensions.cs index 1f381118301b..a55d83af3924 100644 --- a/src/Cli/dotnet/Commands/Tool/Common/ToolManifestFinderExtensions.cs +++ b/src/Cli/dotnet/Commands/Tool/Common/ToolManifestFinderExtensions.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.ToolManifest; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; @@ -12,10 +10,11 @@ namespace Microsoft.DotNet.Cli.Commands.Tool; internal static class ToolManifestFinderExtensions { - public static (FilePath? filePath, string warningMessage) ExplicitManifestOrFindManifestContainPackageId( + public static (FilePath? filePath, string? warningMessage) ExplicitManifestOrFindManifestContainPackageId( this IToolManifestFinder toolManifestFinder, - string explicitManifestFile, - PackageId packageId) + string? explicitManifestFile, + PackageId packageId, + bool throwIfNoManifestFound = true) { if (!string.IsNullOrWhiteSpace(explicitManifestFile)) { @@ -29,12 +28,16 @@ public static (FilePath? filePath, string warningMessage) ExplicitManifestOrFind } catch (ToolManifestCannotBeFoundException e) { - throw new GracefulException([e.Message, CliCommandStrings.ToolCommonNoManifestGuide], verboseMessages: [e.VerboseMessage], isUserError: false); + if (throwIfNoManifestFound) + { + throw new GracefulException([e.Message, CliCommandStrings.ToolCommonNoManifestGuide], verboseMessages: [e.VerboseMessage], isUserError: false); + } + return (null, null); } if (manifestFilesContainPackageId.Any()) { - string warning = null; + string? warning = null; if (manifestFilesContainPackageId.Count > 1) { warning = diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs index 5ee05b4fa10d..1214c75fdafc 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs @@ -102,17 +102,15 @@ public override int Execute() private int ExecuteInstallCommand(PackageId packageId, VersionRange? versionRange) { - FilePath manifestFile = GetManifestFilePath(); - - (FilePath? manifestFileOptional, string warningMessage) = - _toolManifestFinder.ExplicitManifestOrFindManifestContainPackageId(_explicitManifestFile, packageId); + (FilePath? manifestFileOptional, string? warningMessage) = + _toolManifestFinder.ExplicitManifestOrFindManifestContainPackageId(_explicitManifestFile, packageId, throwIfNoManifestFound: false); if (warningMessage != null) { _reporter.WriteLine(warningMessage.Yellow()); } - manifestFile = manifestFileOptional ?? GetManifestFilePath(); + FilePath manifestFile = manifestFileOptional ?? GetManifestFilePath(); var existingPackageWithPackageId = _toolManifestFinder.Find(manifestFile).Where(p => p.PackageId.Equals(packageId)); if (!existingPackageWithPackageId.Any()) diff --git a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallLocalCommandTests.cs b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallLocalCommandTests.cs index 7c5cbd177e8e..2a6fe44ce74e 100644 --- a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallLocalCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallLocalCommandTests.cs @@ -504,6 +504,36 @@ public void GivenNoManifestFileAndCreateManifestIfNeededFlagItShouldCreateManife _fileSystem.File.Exists(Path.Combine(_temporaryDirectory, "dotnet-tools.json")).Should().BeTrue(); } + [Fact] + public void GivenExistingEmptyManifestItShouldInstallToolSuccessfully() + { + // Regression test for https://github.com/dotnet/sdk/issues/[issue-number] + // This test verifies that installing a tool into an existing empty manifest + // (like one created by 'dotnet new tool-manifest') works correctly. + // The bug was that ExplicitManifestOrFindManifestContainPackageId would throw + // when no manifest contained the package, even though a manifest existed. + + // The test setup already creates an empty manifest in _jsonContent, + // so we just need to verify installation works + ParseResult parseResult = Parser.Parse($"dotnet tool install {_packageIdA.ToString()}"); + + var installLocalCommand = new ToolInstallLocalCommand( + parseResult, + _toolPackageDownloaderMock, + _toolManifestFinder, + _toolManifestEditor, + _localToolsResolverCache, + _reporter); + + // This should succeed without throwing NullReferenceException + installLocalCommand.Execute().Should().Be(0); + + // Verify the tool was actually installed to the manifest + var manifestPackages = _toolManifestFinder.Find(); + manifestPackages.Should().HaveCount(1); + manifestPackages.Single().PackageId.Should().Be(_packageIdA); + } + private IToolPackageDownloader GetToolToolPackageInstallerWithPreviewInFeed() { List feeds = new()