@@ -295,10 +295,11 @@ public CSharpRewriteRpc get() {
295295 );
296296 }
297297 } else {
298- // Run via dotnet tool exec with the pinned version from the build
298+ // Install and run the tool from a persistent tool-path, bypassing
299+ // dotnet tool exec which has auth issues with private feeds (dotnet/sdk#51375)
299300 String version = StringUtils .readFully (
300301 CSharpRewriteRpc .class .getResourceAsStream ("/META-INF/rewrite-csharp-version.txt" )).trim ();
301- cmd = buildToolExecCommand (version );
302+ cmd = buildToolPathCommand (version );
302303 }
303304
304305 return startProcess (cmd );
@@ -351,29 +352,74 @@ private CSharpRewriteRpc startProcess(Stream<@Nullable String> cmd) {
351352 );
352353 }
353354
354- private Stream <@ Nullable String > buildToolExecCommand (String version ) {
355- // When the tool package exists in the NuGet global cache (e.g. from pTML),
356- // add it as a source so dotnet tool exec can resolve it without remote feeds
357- Path globalCachePath = Paths .get (System .getProperty ("user.home" ),
358- ".nuget" , "packages" , NUGET_PACKAGE_ID .toLowerCase (), version );
359- String addSource = Files .isDirectory (globalCachePath ) ? globalCachePath .toString () : null ;
355+ /**
356+ * Ensures the tool is installed at a persistent tool-path and returns a command
357+ * to run it directly. This bypasses {@code dotnet tool exec} entirely, working
358+ * around https://github.com/dotnet/sdk/issues/51375 where {@code dotnet tool exec}
359+ * fails to authenticate against private NuGet feeds.
360+ * <p>
361+ * Uses {@code dotnet tool install --tool-path} which handles authentication
362+ * correctly. The tool-path is version-specific so multiple versions can coexist
363+ * without file-lock conflicts during parallel execution.
364+ */
365+ private Stream <@ Nullable String > buildToolPathCommand (String version ) {
366+ Path toolPath = Paths .get (System .getProperty ("user.home" ),
367+ ".dotnet" , "rewrite-tools" , version );
368+ Path toolExecutable = toolPath .resolve (TOOL_COMMAND );
369+
370+ if (!Files .isRegularFile (toolExecutable )) {
371+ installTool (version , toolPath );
372+ }
360373
361374 return Stream .of (
362- dotnetPath .toString (),
363- "tool" , "exec" ,
364- NUGET_PACKAGE_ID + "@" + version ,
365- "-y" ,
366- "--allow-roll-forward" ,
367- addSource != null ? "--add-source" : null ,
368- addSource ,
369- "--ignore-failed-sources" ,
370- // Suppress NuGet informational messages (e.g. "Skipping NuGet package
371- // signature verification") that would corrupt the RPC stdout channel.
372- "-v" , "q" ,
373- "--" ,
375+ toolExecutable .toAbsolutePath ().normalize ().toString (),
374376 log == null ? null : "--log-file=" + log .toAbsolutePath ().normalize (),
375377 traceRpcMessages ? "--trace-rpc-messages" : null
376378 );
377379 }
380+
381+ private void installTool (String version , Path toolPath ) {
382+ try {
383+ Files .createDirectories (toolPath );
384+
385+ List <String > installCmd = new ArrayList <>(Arrays .asList (
386+ dotnetPath .toString (),
387+ "tool" , "install" ,
388+ NUGET_PACKAGE_ID ,
389+ "--version" , version ,
390+ "--tool-path" , toolPath .toString (),
391+ "--ignore-failed-sources"
392+ ));
393+
394+ // When the tool package exists in the NuGet global cache (e.g. from publishToMavenLocal),
395+ // add it as a source so the install can resolve it without remote feeds
396+ Path globalCachePath = Paths .get (System .getProperty ("user.home" ),
397+ ".nuget" , "packages" , NUGET_PACKAGE_ID .toLowerCase (), version );
398+ if (Files .isDirectory (globalCachePath )) {
399+ installCmd .addAll (Arrays .asList ("--add-source" , globalCachePath .toString ()));
400+ }
401+
402+ ProcessBuilder pb = new ProcessBuilder (installCmd );
403+ if (workingDirectory != null ) {
404+ pb .directory (workingDirectory .toFile ());
405+ }
406+ pb .environment ().putAll (environment );
407+ pb .redirectErrorStream (true );
408+ Process process = pb .start ();
409+ String output = StringUtils .readFully (process .getInputStream ());
410+ int exitCode = process .waitFor ();
411+
412+ if (exitCode != 0 ) {
413+ throw new RuntimeException (
414+ "Failed to install " + NUGET_PACKAGE_ID + "@" + version +
415+ " to " + toolPath + " (exit code " + exitCode + "): " + output );
416+ }
417+ } catch (IOException e ) {
418+ throw new UncheckedIOException ("Failed to install " + NUGET_PACKAGE_ID + "@" + version , e );
419+ } catch (InterruptedException e ) {
420+ Thread .currentThread ().interrupt ();
421+ throw new RuntimeException ("Interrupted while installing " + NUGET_PACKAGE_ID + "@" + version , e );
422+ }
423+ }
378424 }
379425}
0 commit comments