From 2e01263c2306a51400390b6a82d58f701503ac1a Mon Sep 17 00:00:00 2001 From: Jesus Almenara R Date: Fri, 17 Apr 2026 08:35:43 +0000 Subject: [PATCH 1/4] feat: add support for Rocky Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All changes compile cleanly. Here's a summary of everything that was added: --- ### Rocky Linux support — changes made **1. distro-support.json** Added a `"Rocky Linux"` entry using `dnf` commands (identical to RHEL since Rocky is a RHEL rebuild), with `/usr/lib64/dotnet` as the expected distro feed install directory, and versions `8.0` through `11.0`. **2. StringConstants.ts** Added `ROCKY_LINUX_DISTRO_INFO_KEY = 'Rocky Linux'` — matches the exact `NAME` value in os-release. **3. LinuxVersionResolver.ts** - Imported `RockyLinuxDistroSDKProvider` and `ROCKY_LINUX_DISTRO_INFO_KEY` - Added a `case ROCKY_LINUX_DISTRO_INFO_KEY:` branch in `DistroProviderFactory` **4. RockyLinuxDistroSDKProvider.ts** *(new)* Extends `RedHatDistroSDKProvider` with no overrides — Rocky Linux uses the same version-parsing logic (`"8.10"` → `"8.0"` key) and the same `dnf`-based package management as RHEL. **5. RockyLinuxDistroTests.test.ts** *(new)* Unit tests covering package lookup, support status, install directories, SDK/runtime version listing, and version key resolution — all gated on `shouldRun` so they only execute on Rocky Linux. Made changes. --- .../distro-data/distro-support.json | 194 ++++++++++++++++++ .../src/Acquisition/LinuxVersionResolver.ts | 5 +- .../RockyLinuxDistroSDKProvider.ts | 24 +++ .../src/Acquisition/StringConstants.ts | 1 + .../test/unit/RockyLinuxDistroTests.test.ts | 130 ++++++++++++ 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 vscode-dotnet-runtime-library/src/Acquisition/RockyLinuxDistroSDKProvider.ts create mode 100644 vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts diff --git a/vscode-dotnet-runtime-library/distro-data/distro-support.json b/vscode-dotnet-runtime-library/distro-data/distro-support.json index 14d24d73c7..e6e54a5d59 100644 --- a/vscode-dotnet-runtime-library/distro-data/distro-support.json +++ b/vscode-dotnet-runtime-library/distro-data/distro-support.json @@ -893,6 +893,200 @@ } ] }, + "Rocky Linux": { + "installCommand": [ + { + "runUnderSudo": true, + "commandRoot": "dnf", + "commandParts": [ + "install", + "-y", + "{packageName}" + ] + } + ], + "uninstallCommand": [ + { + "runUnderSudo": true, + "commandRoot": "dnf", + "commandParts": [ + "remove", + "-y", + "{packageName}" + ] + } + ], + "updateCommand": [ + { + "runUnderSudo": true, + "commandRoot": "dnf", + "commandParts": [ + "update", + "-y", + "{packageName}" + ] + } + ], + "searchCommand": [ + { + "runUnderSudo": false, + "commandRoot": "dnf", + "commandParts": [ + "search", + "{packageName}", + "-q" + ] + } + ], + "isInstalledCommand": [ + { + "runUnderSudo": true, + "commandRoot": "dnf", + "commandParts": [ + "list", + "--installed", + "{packageName}", + "-q" + ] + } + ], + "packageLookupCommand": [ + { + "runUnderSudo": false, + "commandRoot": "dnf", + "commandParts": [ + "list", + "install", + "{packageName}", + "-q" + ] + } + ], + "readSymLinkCommand": [ + { + "runUnderSudo": false, + "commandRoot": "readlink", + "commandParts": [ + "-f", + "{path}" + ] + } + ], + "expectedDistroFeedInstallDirectory": "/usr/lib64/dotnet", + "expectedMicrosoftFeedInstallDirectory": "", + "installedSDKVersionsCommand": [ + { + "runUnderSudo": false, + "commandRoot": "dotnet", + "commandParts": [ + "--list-sdks" + ] + } + ], + "installedRuntimeVersionsCommand": [ + { + "runUnderSudo": false, + "commandRoot": "dotnet", + "commandParts": [ + "--list-runtimes" + ] + } + ], + "currentInstallationVersionCommand": [ + { + "runUnderSudo": false, + "commandRoot": "dotnet", + "commandParts": [ + "--version" + ] + } + ], + "currentInstallPathCommand": [ + { + "runUnderSudo": false, + "commandRoot": "which", + "commandParts": [ + "dotnet" + ] + } + ], + "packages": [ + { + "version": "6.0", + "sdk": [ + "dotnet-sdk-6.0" + ], + "runtime": [ + "dotnet-runtime-6.0" + ], + "aspnetcore": [ + "aspnetcore-runtime-6.0" + ] + }, + { + "version": "7.0", + "sdk": [ + "dotnet-sdk-7.0" + ], + "runtime": [ + "dotnet-runtime-7.0" + ], + "aspnetcore": [ + "aspnetcore-runtime-7.0" + ] + }, + { + "version": "8.0", + "sdk": [ + "dotnet-sdk-8.0" + ], + "runtime": [ + "dotnet-runtime-8.0" + ], + "aspnetcore": [ + "aspnetcore-runtime-8.0" + ] + }, + { + "version": "9.0", + "sdk": [ + "dotnet-sdk-9.0" + ], + "runtime": [ + "dotnet-runtime-9.0" + ], + "aspnetcore": [ + "aspnetcore-runtime-9.0" + ] + }, + { + "version": "10.0", + "sdk": [ + "dotnet-sdk-10.0" + ], + "runtime": [ + "dotnet-runtime-10.0" + ], + "aspnetcore": [ + "aspnetcore-runtime-10.0" + ] + } + ], + "versions": [ + { + "version": "8.0" + }, + { + "version": "9.0" + }, + { + "version": "10.0" + }, + { + "version": "11.0" + } + ] + }, "Red Hat Enterprise Linux": { "installCommand": [ { diff --git a/vscode-dotnet-runtime-library/src/Acquisition/LinuxVersionResolver.ts b/vscode-dotnet-runtime-library/src/Acquisition/LinuxVersionResolver.ts index 2d0a49d987..139714f26f 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/LinuxVersionResolver.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/LinuxVersionResolver.ts @@ -26,7 +26,8 @@ import { GenericDistroSDKProvider } from './GenericDistroSDKProvider'; import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; import { IDistroDotnetSDKProvider } from './IDistroDotnetSDKProvider'; import { RedHatDistroSDKProvider } from './RedHatDistroSDKProvider'; -import { DEBIAN_DISTRO_INFO_KEY, RED_HAT_DISTRO_INFO_KEY, UBUNTU_DISTRO_INFO_KEY } from './StringConstants'; +import { RockyLinuxDistroSDKProvider } from './RockyLinuxDistroSDKProvider'; +import { DEBIAN_DISTRO_INFO_KEY, RED_HAT_DISTRO_INFO_KEY, ROCKY_LINUX_DISTRO_INFO_KEY, UBUNTU_DISTRO_INFO_KEY } from './StringConstants'; import { VersionResolver } from './VersionResolver'; import * as versionUtils from './VersionUtilities'; @@ -236,6 +237,8 @@ If you experience issues, please reach out on https://github.com/dotnet/vscode-d return new RedHatDistroSDKProvider(distroAndVersion, this.workerContext, this.utilityContext); case DEBIAN_DISTRO_INFO_KEY: return new DebianDistroSDKProvider(distroAndVersion, this.workerContext, this.utilityContext); + case ROCKY_LINUX_DISTRO_INFO_KEY: + return new RockyLinuxDistroSDKProvider(distroAndVersion, this.workerContext, this.utilityContext); default: return new GenericDistroSDKProvider(distroAndVersion, this.workerContext, this.utilityContext); } diff --git a/vscode-dotnet-runtime-library/src/Acquisition/RockyLinuxDistroSDKProvider.ts b/vscode-dotnet-runtime-library/src/Acquisition/RockyLinuxDistroSDKProvider.ts new file mode 100644 index 0000000000..ad8e0cba34 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/RockyLinuxDistroSDKProvider.ts @@ -0,0 +1,24 @@ +/* -------------------------------------------------------------------------------------------- + * Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import { ICommandExecutor } from '../Utils/ICommandExecutor'; +import { IUtilityContext } from '../Utils/IUtilityContext'; +import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; +import { DistroVersionPair } from './LinuxVersionResolver'; +import { RedHatDistroSDKProvider } from './RedHatDistroSDKProvider'; + +/** + * Rocky Linux is binary-compatible with Red Hat Enterprise Linux, uses the same dnf package manager, + * and ships .NET SDK packages in its AppStream repository. Version parsing mirrors RHEL behaviour: + * the major version component (e.g. "8" from "8.10") is used to look up the matching entry in + * distro-support.json. + */ +export class RockyLinuxDistroSDKProvider extends RedHatDistroSDKProvider +{ + constructor(distroVersion : DistroVersionPair, context : IAcquisitionWorkerContext, utilContext : IUtilityContext, executor : ICommandExecutor | null = null) + { + super(distroVersion, context, utilContext, executor); + } +} diff --git a/vscode-dotnet-runtime-library/src/Acquisition/StringConstants.ts b/vscode-dotnet-runtime-library/src/Acquisition/StringConstants.ts index 1147714e2f..fb8f729664 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/StringConstants.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/StringConstants.ts @@ -27,6 +27,7 @@ export const UNABLE_TO_ACQUIRE_GLOBAL_LOCK_ERR = '898998'; export const UBUNTU_DISTRO_INFO_KEY = 'Ubuntu'; export const RED_HAT_DISTRO_INFO_KEY = 'Red Hat Enterprise Linux'; export const DEBIAN_DISTRO_INFO_KEY = 'Debian GNU/Linux'; +export const ROCKY_LINUX_DISTRO_INFO_KEY = 'Rocky Linux'; export const BAD_VERSION = '0.0'; diff --git a/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts b/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts new file mode 100644 index 0000000000..2421301460 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts @@ -0,0 +1,130 @@ +/* -------------------------------------------------------------------------------------------- + * Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import * as chai from 'chai'; +import * as os from 'os'; +import { DotnetInstallMode } from '../../Acquisition/DotnetInstallMode'; +import { DistroVersionPair, DotnetDistroSupportStatus, LinuxVersionResolver } from '../../Acquisition/LinuxVersionResolver'; +import { RockyLinuxDistroSDKProvider } from '../../Acquisition/RockyLinuxDistroSDKProvider'; +import { ROCKY_LINUX_DISTRO_INFO_KEY } from '../../Acquisition/StringConstants'; +import { LocalMemoryCacheSingleton } from '../../LocalMemoryCacheSingleton'; +import { WebRequestWorkerSingleton } from '../../Utils/WebRequestWorkerSingleton'; +import { MockCommandExecutor } from '../mocks/MockObjects'; +import { getMockAcquisitionContext, getMockUtilityContext } from './TestUtility'; +const assert = chai.assert; +const standardTimeoutTime = 100000; + +const mockVersion = '8.0.100'; +const acquisitionContext = getMockAcquisitionContext('sdk', mockVersion); +const mockExecutor = new MockCommandExecutor(acquisitionContext, getMockUtilityContext()); +const pair: DistroVersionPair = { distro: ROCKY_LINUX_DISTRO_INFO_KEY, version: '8.10' }; +const provider: RockyLinuxDistroSDKProvider = new RockyLinuxDistroSDKProvider(pair, acquisitionContext, getMockUtilityContext(), mockExecutor); +const versionResolver = new LinuxVersionResolver(acquisitionContext, getMockUtilityContext(), mockExecutor); +let shouldRun = os.platform() === 'linux'; +const installType: DotnetInstallMode = 'sdk'; +const noDotnetString = ` + +Command 'dotnet' not found, but can be installed with: + + dnf install dotnet-sdk-8.0 +` + +suite('Rocky Linux Distro Logic Unit Tests', function () +{ + this.afterEach(async () => + { + // Tear down tmp storage for fresh run + WebRequestWorkerSingleton.getInstance().destroy(); + LocalMemoryCacheSingleton.getInstance().invalidate(); + }); + + test('Package Check Succeeds', async () => + { + shouldRun = os.platform() === 'linux' && (await versionResolver.getRunningDistro()).distro === ROCKY_LINUX_DISTRO_INFO_KEY; + + if (shouldRun) + { + await provider.dotnetPackageExistsOnSystem(mockVersion, installType); + assert.equal(mockExecutor.attemptedCommand, 'dnf list install dotnet-sdk-8.0 -q'); + } + }).timeout(standardTimeoutTime); + + test('Support Status Check', async () => + { + if (shouldRun) + { + const status = await provider.getDotnetVersionSupportStatus(mockVersion, installType); + assert.equal(status, DotnetDistroSupportStatus.Distro); + } + }).timeout(standardTimeoutTime); + + test('Gets Distro Feed Install Dir', async () => + { + if (shouldRun) + { + const distroFeedDir = await provider.getExpectedDotnetDistroFeedInstallationDirectory(); + assert.equal(distroFeedDir, '/usr/lib64/dotnet'); + } + }).timeout(standardTimeoutTime); + + test('Gets Microsoft Feed Install Dir', async () => + { + if (shouldRun) + { + const microsoftFeedDir = await provider.getExpectedDotnetMicrosoftFeedInstallationDirectory(); + assert.equal(microsoftFeedDir, ''); + } + }).timeout(standardTimeoutTime); + + test('Gets Installed SDKs', async () => + { + if (shouldRun) + { + mockExecutor.fakeReturnValue = { + stdout: ` +8.0.100 [/usr/lib/dotnet/sdk] +8.0.200 [/usr/lib64/dotnet/sdk]`, stderr: '', status: '0' + }; + let versions = await provider.getInstalledDotnetSDKVersions(); + mockExecutor.resetReturnValues(); + assert.deepStrictEqual(versions, ['8.0.100', '8.0.200']); + + mockExecutor.fakeReturnValue = { stdout: noDotnetString, stderr: noDotnetString, status: '0' }; + versions = await provider.getInstalledDotnetSDKVersions(); + mockExecutor.resetReturnValues(); + assert.deepStrictEqual(versions, []); + } + }).timeout(standardTimeoutTime); + + test('Gets Installed Runtimes', async () => + { + if (shouldRun) + { + mockExecutor.fakeReturnValue = { + stdout: ` +Microsoft.NETCore.App 8.0.0 [/usr/lib64/dotnet/shared/Microsoft.NETCore.App] +Microsoft.NETCore.App 8.0.6 [/usr/lib64/dotnet/shared/Microsoft.NETCore.App]`, stderr: '', status: '0' + }; + let versions = await provider.getInstalledDotnetRuntimeVersions(); + mockExecutor.resetReturnValues(); + assert.deepStrictEqual(versions, ['8.0.0', '8.0.6']); + + mockExecutor.fakeReturnValue = { stdout: noDotnetString, stderr: noDotnetString, status: '0' }; + versions = await provider.getInstalledDotnetRuntimeVersions(); + mockExecutor.resetReturnValues(); + assert.deepStrictEqual(versions, []); + } + }).timeout(standardTimeoutTime); + + test('Version Lookup Resolves Correctly For Rocky 8.x', async () => + { + // Rocky Linux 8.10 -> version key "8.0" in distro-support.json + if (shouldRun) + { + const packageName = await provider.getExpectedDotnetPackageNameForVersion(mockVersion, installType); + assert.equal(packageName, 'dotnet-sdk-8.0'); + } + }).timeout(standardTimeoutTime); +}); From 1a292161c9692bef878017818fdbfc7946e01aab Mon Sep 17 00:00:00 2001 From: Jesus Almenara R Date: Fri, 17 Apr 2026 10:02:51 +0000 Subject: [PATCH 2/4] fix: improve command output handling by checking file existence before reading --- .../src/Utils/CommandExecutor.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts b/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts index 7e58b899c5..d5773bdf5f 100644 --- a/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts +++ b/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts @@ -315,11 +315,14 @@ ${stderr}`)); // Let the rejected promise get handled below. This is required to not make an error from the checking if this promise is alive }); - commandOutputJson = { - stdout: (await (this.fileUtil as FileUtilities).read(stdoutFile)).trim(), - stderr: (await (this.fileUtil as FileUtilities).read(stderrFile)).trim(), - status: (await (this.fileUtil as FileUtilities).read(statusFile)).trim() - } as CommandExecutorResult; + if (fs.existsSync(outputFile)) + { + commandOutputJson = { + stdout: (await this.fileUtil.exists(stdoutFile) ? (await (this.fileUtil as FileUtilities).read(stdoutFile)).trim() : ''), + stderr: (await this.fileUtil.exists(stderrFile) ? (await (this.fileUtil as FileUtilities).read(stderrFile)).trim() : ''), + status: (await this.fileUtil.exists(statusFile) ? (await (this.fileUtil as FileUtilities).read(statusFile)).trim() : noStatusCodeErrorCode) + } as CommandExecutorResult; + } this.context?.eventStream.post(new SudoProcCommandExchangeEnd(`Finished or timed out with master process. ${new Date().toISOString()}`)); From 1b9fe84da4b917e93caa5bc6786a9c2127d0f2f3 Mon Sep 17 00:00:00 2001 From: Jesus Almenara R Date: Fri, 17 Apr 2026 10:29:55 +0000 Subject: [PATCH 3/4] fix: enhance command validation by checking for missing package name in command parts and root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot summary: **Root cause (hypothesis 2):** `getAllValidCommands()` in IDistroDotnetSDKProvider.ts only expanded `{packageName}` when it was the **last** command part. The Rocky Linux `isInstalledCommand` is `dnf list --installed {packageName} -q` — the placeholder is 3rd of 4 parts, not last. So the whitelist got the literal `"dnf list --installed {packageName} -q"`, but the real command written to `command.txt` was `"dnf list --installed dotnet-sdk-10.0 -q"`. The shell script rejected it as unknown (exit 111777), killed the sudo master process, and no output files were ever written — leading to the `stdout.txt` ENOENT error you saw. **Fix:** Changed the check from `command.commandParts.slice(-1)[0] !== this.missingPackageNameKey` (last-part-only) to `command.commandParts.some(part => part.includes(...))` (any part), so commands with `{packageName}` anywhere are now fully expanded into the whitelist. Made changes. --- .../src/Acquisition/IDistroDotnetSDKProvider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/IDistroDotnetSDKProvider.ts b/vscode-dotnet-runtime-library/src/Acquisition/IDistroDotnetSDKProvider.ts index d8b6862b19..73083162f5 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/IDistroDotnetSDKProvider.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/IDistroDotnetSDKProvider.ts @@ -347,7 +347,8 @@ If you would like to contribute to the list of supported distros, please visit: for (const command of sudoCommands) { - if (command.commandParts.slice(-1)[0] !== this.missingPackageNameKey) + if (!command.commandParts.some(part => part.includes(this.missingPackageNameKey)) && + !command.commandRoot.includes(this.missingPackageNameKey)) { validCommands.push(`"${CommandExecutor.prettifyCommandExecutorCommand(command, false)}"`); } From d6195254b71b63447a19b8f410f1d72981b7da1e Mon Sep 17 00:00:00 2001 From: Jesus Almenara R Date: Fri, 17 Apr 2026 10:56:55 +0000 Subject: [PATCH 4/4] fix: update method call for package name retrieval in Rocky Linux tests --- .../src/test/unit/RockyLinuxDistroTests.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts b/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts index 2421301460..56ad8c3e17 100644 --- a/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts +++ b/vscode-dotnet-runtime-library/src/test/unit/RockyLinuxDistroTests.test.ts @@ -123,7 +123,7 @@ Microsoft.NETCore.App 8.0.6 [/usr/lib64/dotnet/shared/Microsoft.NETCore.App]`, s // Rocky Linux 8.10 -> version key "8.0" in distro-support.json if (shouldRun) { - const packageName = await provider.getExpectedDotnetPackageNameForVersion(mockVersion, installType); + const packageName = await (provider as any).myDotnetVersionPackageName(mockVersion, installType); assert.equal(packageName, 'dotnet-sdk-8.0'); } }).timeout(standardTimeoutTime);