Skip to content

Commit 955a86b

Browse files
authored
Work around dotnet tool exec auth failure on private NuGet feeds (#7347)
dotnet tool exec has a known bug (dotnet/sdk#51375) where it fails to authenticate against private NuGet feeds, returning 401 even when credentials are properly configured. Replace dotnet tool exec with dotnet tool install --tool-path + direct execution, which handles authentication correctly. Tool-paths are version-specific to avoid file-lock conflicts during parallel execution.
1 parent 41f78a2 commit 955a86b

1 file changed

Lines changed: 66 additions & 20 deletions

File tree

rewrite-csharp/src/main/java/org/openrewrite/csharp/rpc/CSharpRewriteRpc.java

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)