Repository: pre-commit/pre-commit

Issue:
dotnet install fails for prefixed packages
### search you tried in the issue tracker

dotnet tool

### describe your issue

A bit of an oversight when constructing `tool_name` here:

https://github.com/pre-commit/pre-commit/blob/cb0bcfd67fc35e91f7b2eca7e33bceda459dca77/pre_commit/languages/dotnet.py#L60-L63

E.g.

```console
$ pre-commit try-repo https://github.com/rkm/sample-dotnet-tool
[INFO] Initializing environment for https://github.com/rkm/sample-dotnet-tool.
===============================================================================
Using config:
===============================================================================
repos:
-   repo: https://github.com/rkm/sample-dotnet-tool
    rev: e53a3601bc06bb038dac30da813572291dd8d58f
    hooks:
    -   id: sample-dotnet-tool
===============================================================================
[INFO] Installing environment for https://github.com/rkm/sample-dotnet-tool.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
An unexpected error has occurred: CalledProcessError: command: ('/home/rkm/bin/dotnet', 'tool', 'install', '--tool-path', '/tmp/tmp6bk4v26x/repotefhurdg/dotnetenv-default/bin', '--add-source', 'pre-commit-build', 'Rkm')
return code: 1
expected return code: 0
stdout:
    /tmp/1873db78-d0a7-48ba-bbff-10a7ef85a2a6/restore.csproj : error NU1101: Unable to find package rkm. No packages exist with this id in source(s): /tmp/tmp6bk4v26x/repotefhurdg/pre-commit-build, nuget.org

stderr:
    The tool package could not be restored.
    Tool 'rkm' failed to install. This failure may have been caused by:

    * You are attempting to install a preview release and did not use the --version option to specify the version.
    * A package by this name was found, but it was not a .NET tool.
    * The required NuGet feed cannot be accessed, perhaps because of an Internet connection problem.
    * You mistyped the name of the tool.

    For more reasons, including package naming enforcement, visit https://aka.ms/failure-installing-tool

Check the log at /home/rkm/.cache/pre-commit/pre-commit.log
```

### pre-commit --version

pre-commit 2.20.0

### .pre-commit-config.yaml

```yaml
repos:
-   repo: https://github.com/rkm/sample-dotnet-tool
    rev: e53a3601bc06bb038dac30da813572291dd8d58f
    hooks:
    -   id: sample-dotnet-tool
```


### ~/.cache/pre-commit/pre-commit.log (if present)

_No response_


Discussion:

Resolving patch:
diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py
--- a/pre_commit/languages/dotnet.py
+++ b/pre_commit/languages/dotnet.py
@@ -2,6 +2,9 @@
 
 import contextlib
 import os.path
+import re
+import xml.etree.ElementTree
+import zipfile
 from typing import Generator
 from typing import Sequence
 
@@ -57,10 +60,29 @@ def install_environment(
             ),
         )
 
-        # Determine tool from the packaged file <tool_name>.<version>.nupkg
-        build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir))
-        for output in build_outputs:
-            tool_name = output.split('.')[0]
+        nupkg_dir = prefix.path(build_dir)
+        nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')]
+
+        if not nupkgs:
+            raise AssertionError('could not find any build outputs to install')
+
+        for nupkg in nupkgs:
+            with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f:
+                nuspec, = (x for x in f.namelist() if x.endswith('.nuspec'))
+                with f.open(nuspec) as spec:
+                    tree = xml.etree.ElementTree.parse(spec)
+
+            namespace = re.match(r'{.*}', tree.getroot().tag)
+            if not namespace:
+                raise AssertionError('could not parse namespace from nuspec')
+
+            tool_id_element = tree.find(f'.//{namespace[0]}id')
+            if tool_id_element is None:
+                raise AssertionError('expected to find an "id" element')
+
+            tool_id = tool_id_element.text
+            if not tool_id:
+                raise AssertionError('"id" element missing tool name')
 
             # Install to bin dir
             helpers.run_setup_cmd(
@@ -69,7 +91,7 @@ def install_environment(
                     'dotnet', 'tool', 'install',
                     '--tool-path', os.path.join(envdir, BIN_DIR),
                     '--add-source', build_dir,
-                    tool_name,
+                    tool_id,
                 ),
             )
 


Test patch:
diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore
new file mode 100644
--- /dev/null
+++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore
@@ -0,0 +1,3 @@
+bin/
+obj/
+nupkg/
diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml
new file mode 100644
--- /dev/null
+++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+-   id: dotnet-example-hook
+    name: dotnet example hook
+    entry: testeroni.tool
+    language: dotnet
+    files: ''
diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs
new file mode 100644
--- /dev/null
+++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace dotnet_hooks_repo
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {
+            Console.WriteLine("Hello from dotnet!");
+        }
+    }
+}
diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj
new file mode 100644
--- /dev/null
+++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net7.0</TargetFramework>
+    <PackAsTool>true</PackAsTool>
+    <ToolCommandName>testeroni.tool</ToolCommandName>
+    <PackageOutputPath>./nupkg</PackageOutputPath>
+  </PropertyGroup>
+</Project>
diff --git a/tests/repository_test.py b/tests/repository_test.py
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -1031,6 +1031,7 @@ def test_local_perl_additional_dependencies(store):
         'dotnet_hooks_csproj_repo',
         'dotnet_hooks_sln_repo',
         'dotnet_hooks_combo_repo',
+        'dotnet_hooks_csproj_prefix_repo',
     ),
 )
 def test_dotnet_hook(tempdir_factory, store, repo):