From 3e0d38dcf2439c799651f9c066638955ca152d11 Mon Sep 17 00:00:00 2001 From: Phil Henning Date: Mon, 26 Jan 2026 16:48:34 -0500 Subject: [PATCH] Add SDK version utilities and GlobalJson parsing Add new utilities for SDK version parsing and comparison: - parseSdkVersion: Parse SDK version string into components - isCompatibleSdkVersion: Check version compatibility with rollForward policy - getCompatibleSdkVersions: Filter versions by compatibility - compareSdkVersions: Compare two SDK versions - isNewerSdkVersion: Check if one version is newer than another - isValidNumber: Exported helper for number validation Add GlobalJson parsing utilities: - GlobalJson, GlobalJsonSdk, GlobalJsonRequirements interfaces - parseGlobalJsonContent: Parse global.json content - getRequirementsFromGlobalJson: Extract SDK requirements - parseGlobalJsonRequirements: Combined parse and extract - getEffectiveRollForward: Get effective roll-forward policy - getEffectiveAllowPrerelease: Get effective prerelease setting Register VS Code commands for external extension consumption: - dotnet.parseSdkVersion - dotnet.isCompatibleSdkVersion - dotnet.getCompatibleSdkVersions - dotnet.compareSdkVersions - dotnet.isNewerSdkVersion - dotnet.parseGlobalJson Add unit tests for all new SDK version utilities. --- .../src/extension.ts | 79 +++++- .../src/Acquisition/GlobalJson.ts | 147 ++++++++++ .../src/Acquisition/VersionUtilities.ts | 257 +++++++++++++++++- vscode-dotnet-runtime-library/src/index.ts | 1 + .../src/test/unit/VersionUtilities.test.ts | 133 +++++++++ 5 files changed, 592 insertions(+), 25 deletions(-) create mode 100644 vscode-dotnet-runtime-library/src/Acquisition/GlobalJson.ts diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index ec8ec502da..535e7f8267 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -13,6 +13,7 @@ import AcquisitionInvoker, callWithErrorHandling, CommandExecutor, + compareSdkVersions, directoryProviderFactory, DotnetAcquisitionMissingLinuxDependencies, DotnetAcquisitionRequested, @@ -43,11 +44,15 @@ import ExistingPathResolver, ExtensionConfigurationWorker, formatIssueUrl, + getCompatibleSdkVersions, getInstallIdCustomArchitecture, getMajor, getMajorMinor, + getRequirementsFromGlobalJson, GlobalAcquisitionContextMenuOpened, GlobalInstallerResolver, + GlobalJson, + GlobalJsonRequirements, IAcquisitionWorkerContext, IDotnetAcquireContext, IDotnetAcquireResult, @@ -67,13 +72,19 @@ import InstallationValidator, InstallRecord, InvalidUninstallRequest, + isCompatibleSdkVersion, + isNewerSdkVersion, IUtilityContext, JsonInstaller, LinuxVersionResolver, LocalInstallUpdateService, LocalMemoryCacheSingleton, NoExtensionIdProvided, + ParsedSdkVersion, + parseGlobalJsonContent, + parseSdkVersion, registerEventStream, + RollForwardPolicy, UninstallErrorConfiguration, UserManualInstallFailure, UserManualInstallRequested, @@ -83,7 +94,7 @@ import VSCodeEnvironment, VSCodeExtensionContext, WebRequestWorkerSingleton, - WindowDisplayWorker + WindowDisplayWorker, } from 'vscode-dotnet-runtime-library'; import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acquisition/InstallTrackerSingleton'; import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId'; @@ -112,19 +123,25 @@ namespace commandKeys export const acquire = 'acquire'; export const acquireGlobalSDK = 'acquireGlobalSDK'; export const acquireStatus = 'acquireStatus'; - export const uninstall = 'uninstall'; + export const availableInstalls = 'availableInstalls'; + export const compareSdkVersions = 'compareSdkVersions'; + export const ensureDotnetDependencies = 'ensureDotnetDependencies'; export const findPath = 'findPath'; - export const uninstallPublic = 'uninstallPublic' - export const uninstallAll = 'uninstallAll'; + export const getCompatibleSdkVersions = 'getCompatibleSdkVersions'; + export const globalAcquireSDKPublic = 'acquireGlobalSDKPublic'; + export const isCompatibleSdkVersion = 'isCompatibleSdkVersion'; + export const isNewerSdkVersion = 'isNewerSdkVersion'; export const listVersions = 'listVersions'; + export const parseGlobalJson = 'parseGlobalJson'; + export const parseSdkVersion = 'parseSdkVersion'; export const recommendedVersion = 'recommendedVersion' - export const globalAcquireSDKPublic = 'acquireGlobalSDKPublic'; - export const showAcquisitionLog = 'showAcquisitionLog'; - export const ensureDotnetDependencies = 'ensureDotnetDependencies'; export const reportIssue = 'reportIssue'; export const resetData = 'resetData'; - export const availableInstalls = 'availableInstalls'; export const resetUpdateTimerInternal = '_resetUpdateTimer'; + export const showAcquisitionLog = 'showAcquisitionLog'; + export const uninstall = 'uninstall'; + export const uninstallAll = 'uninstallAll'; + export const uninstallPublic = 'uninstallPublic' } const commandPrefix = 'dotnet'; @@ -841,6 +858,34 @@ ${JSON.stringify(commandContext)}`)); open(url).catch(() => {}); }); + const compareSdkVersionsRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.compareSdkVersions}`, + (versionA: string, versionB: string): number => compareSdkVersions(versionA, versionB)); + + const getCompatibleSdkVersionsRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.getCompatibleSdkVersions}`, + (installedVersions: string[], requiredVersion: string, rollForward?: RollForwardPolicy): string[] => + getCompatibleSdkVersions(installedVersions, requiredVersion, rollForward)); + + const isCompatibleSdkVersionRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.isCompatibleSdkVersion}`, + (installedVersion: string, requiredVersion: string, rollForward?: RollForwardPolicy): boolean => + isCompatibleSdkVersion(installedVersion, requiredVersion, rollForward)); + + const isNewerSdkVersionRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.isNewerSdkVersion}`, + (versionA: string, versionB: string): boolean => isNewerSdkVersion(versionA, versionB)); + + const parseGlobalJsonRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.parseGlobalJson}`, + (content: string, filePath: string): GlobalJsonRequirements | undefined => + { + const parsed = parseGlobalJsonContent(content); + if (!parsed) + { + return undefined; + } + return getRequirementsFromGlobalJson(parsed, filePath); + }); + + const parseSdkVersionRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.parseSdkVersion}`, + (version: string): ParsedSdkVersion => parseSdkVersion(version)); + // Helper Functions async function resolveExistingPathIfExists(configResolver: ExtensionConfigurationWorker, commandContext: IDotnetAcquireContext, workerContext: IAcquisitionWorkerContext, utilityContext: IUtilityContext, requirement?: DotnetVersionSpecRequirement): Promise @@ -999,22 +1044,28 @@ Installation will timeout in ${timeoutValue} seconds.`)) // Exposing API Endpoints vsCodeContext.subscriptions.push( + acquireGlobalSDKPublicRegistration, + compareSdkVersionsRegistration, + dotnetAcquireGlobalSDKRegistration, dotnetAcquireRegistration, dotnetAcquireStatusRegistration, - dotnetAcquireGlobalSDKRegistration, dotnetAvailableInstallsRegistration, - acquireGlobalSDKPublicRegistration, dotnetFindPathRegistration, + dotnetForceUpdateRegistration, dotnetListVersionsRegistration, dotnetRecommendedVersionRegistration, - dotnetUninstallRegistration, - dotnetUninstallPublicRegistration, dotnetUninstallAllRegistration, - dotnetForceUpdateRegistration, - showOutputChannelRegistration, + dotnetUninstallPublicRegistration, + dotnetUninstallRegistration, ensureDependenciesRegistration, + getCompatibleSdkVersionsRegistration, + isCompatibleSdkVersionRegistration, + isNewerSdkVersionRegistration, + parseGlobalJsonRegistration, + parseSdkVersionRegistration, reportIssueRegistration, resetUpdateTimerInternalRegistration, + showOutputChannelRegistration, ...eventStreamObservers); if (showResetDataCommand) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/GlobalJson.ts b/vscode-dotnet-runtime-library/src/Acquisition/GlobalJson.ts new file mode 100644 index 0000000000..ada2194926 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/GlobalJson.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import { RollForwardPolicy } from './VersionUtilities'; + +/** + * Represents the structure of a global.json file. + * Only includes officially supported properties per: + * https://learn.microsoft.com/dotnet/core/tools/global-json + */ +export interface GlobalJson +{ + sdk?: GlobalJsonSdk; + 'msbuild-sdks'?: Record; +} + +/** + * The SDK section of a global.json file. + */ +export interface GlobalJsonSdk +{ + /** + * The SDK version to use. + * Can be a specific version (e.g., "8.0.308") or a feature band pattern (e.g., "8.0.3xx"). + */ + version?: string; + + /** + * Whether to allow prerelease SDK versions. + * Defaults to false when version is specified, true otherwise. + */ + allowPrerelease?: boolean; + + /** + * The roll-forward policy for SDK version selection. + * Defaults to 'latestPatch' when version is specified, 'latestMajor' otherwise. + */ + rollForward?: RollForwardPolicy; + + /** + * Additional paths to search for SDKs (.NET 10+). + */ + paths?: string[]; + + /** + * Custom error message when SDK requirements cannot be satisfied (.NET 10+). + */ + errorMessage?: string; +} + +/** + * Requirements extracted from a global.json file, with the file path included. + */ +export interface GlobalJsonRequirements +{ + /** Path to the global.json file */ + filePath: string; + /** SDK version required */ + sdkVersion?: string; + /** Whether prerelease SDKs are allowed */ + allowPrerelease?: boolean; + /** Roll-forward policy */ + rollForward?: RollForwardPolicy; +} + +/** + * Parses a global.json content string and returns its contents. + * @param content The JSON content of the global.json file. + * @returns Parsed global.json contents, or undefined if the content is invalid. + */ +export function parseGlobalJsonContent(content: string): GlobalJson | undefined +{ + try + { + return JSON.parse(content) as GlobalJson; + } + catch + { + return undefined; + } +} + +/** + * Extracts SDK requirements from a parsed global.json object. + * @param globalJson The parsed global.json object. + * @param filePath The path to the global.json file (for reference in the result). + * @returns Extracted requirements. + */ +export function getRequirementsFromGlobalJson(globalJson: GlobalJson, filePath: string): GlobalJsonRequirements +{ + return { + filePath, + sdkVersion: globalJson.sdk?.version, + allowPrerelease: globalJson.sdk?.allowPrerelease, + rollForward: globalJson.sdk?.rollForward, + }; +} + +/** + * Parses global.json content and extracts SDK requirements in one step. + * @param content The JSON content of the global.json file. + * @param filePath The path to the global.json file (for reference in the result). + * @returns Extracted requirements, or undefined if the content is invalid. + */ +export function parseGlobalJsonRequirements(content: string, filePath: string): GlobalJsonRequirements | undefined +{ + const globalJson = parseGlobalJsonContent(content); + if (!globalJson) + { + return undefined; + } + return getRequirementsFromGlobalJson(globalJson, filePath); +} + +/** + * Gets the effective roll-forward policy from global.json requirements. + * If not specified, returns the default policy based on whether a version is specified. + * @param requirements The global.json requirements. + * @returns The effective roll-forward policy. + */ +export function getEffectiveRollForward(requirements: GlobalJsonRequirements): RollForwardPolicy +{ + if (requirements.rollForward) + { + return requirements.rollForward; + } + // Default is 'latestPatch' when version is specified, 'latestMajor' otherwise + return requirements.sdkVersion ? 'latestPatch' : 'latestMajor'; +} + +/** + * Gets the effective allowPrerelease setting from global.json requirements. + * If not specified, returns the default based on whether a version is specified. + * @param requirements The global.json requirements. + * @returns The effective allowPrerelease setting. + */ +export function getEffectiveAllowPrerelease(requirements: GlobalJsonRequirements): boolean +{ + if (requirements.allowPrerelease !== undefined) + { + return requirements.allowPrerelease; + } + // Default is false when version is specified, true otherwise + return !requirements.sdkVersion; +} diff --git a/vscode-dotnet-runtime-library/src/Acquisition/VersionUtilities.ts b/vscode-dotnet-runtime-library/src/Acquisition/VersionUtilities.ts index fe3ef5a9cc..ced6de421f 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/VersionUtilities.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/VersionUtilities.ts @@ -19,6 +19,180 @@ import { BAD_VERSION } from './StringConstants'; const invalidFeatureBandErrorString = `A feature band couldn't be determined for the requested version: `; +/** + * Parsed SDK version components. + * SDK versions follow the format: major.minor.patchFull[-prerelease] + * where patchFull = (featureBand * 100) + patch + * e.g., 8.0.308 -> major=8, minor=0, featureBand=3, patch=8 + */ +export interface ParsedSdkVersion +{ + major: number; + minor: number; + featureBand: number; + patch: number; + patchFull: number; + isPrerelease: boolean; + originalVersion: string; +} + +/** + * The rollForward policy from global.json that determines SDK version selection behavior. + * Reference: https://learn.microsoft.com/en-us/dotnet/core/tools/global-json + */ +export type RollForwardPolicy = 'disable' | 'patch' | 'latestPatch' | 'feature' | 'latestFeature' | 'minor' | 'latestMinor' | 'major' | 'latestMajor'; + +/** + * Parse SDK version string into its component parts. + * Handles versions like "8.0.308", "9.0.100-preview.1", etc. + * Uses the pure utility functions for consistency with other version parsing in this file. + * + * @param version The SDK version string + * @returns Parsed version components + */ +export function parseSdkVersion(version: string): ParsedSdkVersion +{ + const baseVersion = version.split('-')[0]; + const majorMinor = getMajorMinorFromValidVersion(baseVersion); + const majorMinorParts = majorMinor === BAD_VERSION ? ['0', '0'] : majorMinor.split('.'); + + const major = parseInt(majorMinorParts[0], 10) || 0; + const minor = parseInt(majorMinorParts[1], 10) || 0; + const patchFull = getFullPatchFromVersionSimple(version) ?? 0; + const featureBand = getFeatureBandFromVersionSimple(version) ?? 0; + const patch = getPatchFromVersionSimple(version) ?? 0; + const isPrerelease = isPreviewVersionSimple(version); + + return { major, minor, featureBand, patch, patchFull, isPrerelease, originalVersion: version }; +} + +/** + * Check if an installed SDK version is compatible with a required version based on the rollForward policy. + * This implements the global.json rollForward behavior as documented at: + * https://learn.microsoft.com/en-us/dotnet/core/tools/global-json + * + * @param installedVersion The installed SDK version string + * @param requiredVersion The required SDK version string (from global.json) + * @param rollForward The rollForward policy (defaults to 'latestPatch' if not specified) + * @returns true if the installed SDK satisfies the requirement + */ +export function isCompatibleSdkVersion(installedVersion: string, requiredVersion: string, rollForward: RollForwardPolicy = 'latestPatch'): boolean +{ + const inst = parseSdkVersion(installedVersion); + const req = parseSdkVersion(requiredVersion); + + // Prerelease of same base version is considered less than release + const instBase = installedVersion.split('-')[0]; + const reqBase = requiredVersion.split('-')[0]; + if (instBase === reqBase && inst.isPrerelease && !req.isPrerelease) + { + return false; + } + + switch (rollForward) + { + case 'disable': + // Exact match required + return installedVersion === requiredVersion; + + case 'patch': + case 'latestPatch': + // Same major.minor.featureBand, patch >= required patch + return inst.major === req.major && + inst.minor === req.minor && + inst.featureBand === req.featureBand && + inst.patch >= req.patch; + + case 'feature': + case 'latestFeature': + // Same major.minor, featureBand >= required (can roll forward to higher feature band) + return inst.major === req.major && + inst.minor === req.minor && + (inst.featureBand > req.featureBand || + (inst.featureBand === req.featureBand && inst.patch >= req.patch)); + + case 'minor': + case 'latestMinor': + // Same major, minor.featureBand.patch >= required + return inst.major === req.major && + (inst.minor > req.minor || + (inst.minor === req.minor && + (inst.featureBand > req.featureBand || + (inst.featureBand === req.featureBand && inst.patch >= req.patch)))); + + case 'major': + case 'latestMajor': + // Any version >= required + return inst.major > req.major || + (inst.major === req.major && + (inst.minor > req.minor || + (inst.minor === req.minor && + (inst.featureBand > req.featureBand || + (inst.featureBand === req.featureBand && inst.patch >= req.patch))))); + + default: + // Unknown policy, fall back to latestPatch behavior + return inst.major === req.major && + inst.minor === req.minor && + inst.featureBand === req.featureBand && + inst.patch >= req.patch; + } +} + +/** + * Filter a list of SDK versions to only those compatible with the required version and rollForward policy. + * + * @param installedVersions List of installed SDK version strings + * @param requiredVersion The required SDK version + * @param rollForward The rollForward policy + * @returns List of compatible SDK versions + */ +export function getCompatibleSdkVersions(installedVersions: string[], requiredVersion: string, rollForward: RollForwardPolicy = 'latestPatch'): string[] +{ + if (!installedVersions || !requiredVersion) + { + return []; + } + return installedVersions.filter(v => isCompatibleSdkVersion(v, requiredVersion, rollForward)); +} + +/** + * Compare two SDK versions to determine which is newer. + * + * @param versionA First version to compare + * @param versionB Second version to compare + * @returns negative if A < B, 0 if A == B, positive if A > B + */ +export function compareSdkVersions(versionA: string, versionB: string): number +{ + const a = parseSdkVersion(versionA); + const b = parseSdkVersion(versionB); + + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + if (a.patchFull !== b.patchFull) return a.patchFull - b.patchFull; + + // Handle prerelease: release > prerelease of same version + if (a.isPrerelease !== b.isPrerelease) + { + return a.isPrerelease ? -1 : 1; + } + + return 0; +} + +/** + * Check if versionA is newer than versionB. + * + * @param versionA Version to check + * @param versionB Version to compare against + * @returns true if versionA is newer than versionB + */ +export function isNewerSdkVersion(versionA: string, versionB: string): boolean +{ + return compareSdkVersions(versionA, versionB) > 0; +} + /** * * @param fullySpecifiedVersion the fully specified version of the sdk, e.g. 7.0.301 to get the major from. @@ -62,11 +236,11 @@ export function getMajorMinor(fullySpecifiedVersion: string, eventStream: IEvent { if (fullySpecifiedVersion.split('.').length < 2) { - if (fullySpecifiedVersion.split('.').length === 0 && isNumber(fullySpecifiedVersion)) + if (fullySpecifiedVersion.split('.').length === 0 && isValidNumber(fullySpecifiedVersion)) { return `${fullySpecifiedVersion}.0`; } - else if (fullySpecifiedVersion.split('.').length === 1 && isNumber(fullySpecifiedVersion.split('.')[0])) + else if (fullySpecifiedVersion.split('.').length === 1 && isValidNumber(fullySpecifiedVersion.split('.')[0])) { return fullySpecifiedVersion; } @@ -127,7 +301,7 @@ export function getFeatureBandPatchVersion(fullySpecifiedVersion: string, eventS export function getSDKPatchVersionString(fullySpecifiedVersion: string, eventStream: IEventStream, context: IAcquisitionWorkerContext, considerErrorIfNoBand = true): string { const patch = getSDKFeatureBandOrPatchFromFullySpecifiedVersion(fullySpecifiedVersion); - if (patch === '' || !isNumber(patch)) + if (patch === '' || !isValidNumber(patch)) { if (considerErrorIfNoBand) { @@ -182,7 +356,7 @@ export function getSDKCompleteBandAndPatchVersionString(fullySpecifiedVersion: s export function getRuntimePatchVersionString(fullySpecifiedVersion: string, eventStream: IEventStream, context: IAcquisitionWorkerContext): string | null { const patch: string | undefined = fullySpecifiedVersion.split('.')?.[2]?.split('-')?.[0]; - if (patch && !isNumber(patch)) + if (patch && !isValidNumber(patch)) { const event = new DotnetInvalidRuntimePatchVersion(new EventCancellationError('DotnetInvalidRuntimePatchVersion', `The runtime patch version ${patch} from ${fullySpecifiedVersion} is NaN.`), @@ -225,10 +399,11 @@ export function isValidLongFormVersionFormat(fullySpecifiedVersion: string, even * * @param fullySpecifiedVersion the requested version to analyze. * @returns true IFF version is of an rc, preview, internal build, etc. + * @remarks This is the eventStream-compatible version. For pure function usage, see isPreviewVersionSimple. */ export function isPreviewVersion(fullySpecifiedVersion: string, eventStream: IEventStream, context: IAcquisitionWorkerContext): boolean { - return fullySpecifiedVersion.includes('-'); + return isPreviewVersionSimple(fullySpecifiedVersion); } /** @@ -239,7 +414,7 @@ export function isPreviewVersion(fullySpecifiedVersion: string, eventStream: IEv export function isNonSpecificFeatureBandedVersion(version: string): boolean { const numberOfPeriods = version.split('.').length - 1; - return version.split('.').slice(0, 2).every(x => isNumber(x)) && version.endsWith('x') && numberOfPeriods === 2; + return version.split('.').slice(0, 2).every(x => isValidNumber(x)) && version.endsWith('x') && numberOfPeriods === 2; } /** @@ -249,7 +424,7 @@ export function isNonSpecificFeatureBandedVersion(version: string): boolean */ export function isFullySpecifiedVersion(version: string, eventStream: IEventStream, context: IAcquisitionWorkerContext): boolean { - return version.split('.').every(x => isNumber(x)) && isValidLongFormVersionFormat(version, eventStream, context) && !isNonSpecificFeatureBandedVersion(version); + return version.split('.').every(x => isValidNumber(x)) && isValidLongFormVersionFormat(version, eventStream, context) && !isNonSpecificFeatureBandedVersion(version); } /** @@ -260,19 +435,79 @@ export function isFullySpecifiedVersion(version: string, eventStream: IEventStre export function isNonSpecificMajorOrMajorMinorVersion(version: string): boolean { const numberOfPeriods = version.split('.').length - 1; - return isNumber(version) && numberOfPeriods >= 0 && numberOfPeriods < 2; + return isValidNumber(version) && numberOfPeriods >= 0 && numberOfPeriods < 2; } /** - * + * Check if a string represents a valid number. * @param value the string to check and see if it's a valid number. * @returns true if it's a valid number. */ -function isNumber(value: string | number): boolean +export function isValidNumber(value: string | number): boolean { return ( (value != null) && (value !== '') && !isNaN(Number(value.toString())) ); -} \ No newline at end of file +} + +// #region Internal helper functions + +/** + * Simple check for prerelease versions. Returns true if version contains '-'. + * @see isPreviewVersion for the version used in acquisition workflows that requires eventStream + */ +function isPreviewVersionSimple(version: string): boolean +{ + return version.includes('-'); +} + +/** + * Gets the full patch field from an SDK version string (e.g., 308 from "8.0.308"). + * @returns The full patch number, or undefined if it cannot be extracted + */ +function getFullPatchFromVersionSimple(version: string): number | undefined +{ + // Remove prerelease suffix if present + const dashIndex = version.indexOf('-'); + const versionWithoutPrerelease = dashIndex >= 0 ? version.substring(0, dashIndex) : version; + + const parts = versionWithoutPrerelease.split('.'); + if (parts.length < 3 || !isValidNumber(parts[2])) + { + return undefined; + } + + return parseInt(parts[2], 10); +} + +/** + * Gets the feature band from an SDK version string (e.g., 3 from "8.0.308"). + * @returns The feature band number, or undefined if it cannot be extracted + */ +function getFeatureBandFromVersionSimple(version: string): number | undefined +{ + const patchFull = getFullPatchFromVersionSimple(version); + if (patchFull === undefined) + { + return undefined; + } + return Math.floor(patchFull / 100); +} + +/** + * Gets the patch within the feature band from an SDK version string (e.g., 8 from "8.0.308"). + * @returns The patch number, or undefined if it cannot be extracted + */ +function getPatchFromVersionSimple(version: string): number | undefined +{ + const patchFull = getFullPatchFromVersionSimple(version); + if (patchFull === undefined) + { + return undefined; + } + return patchFull % 100; +} + +// #endregion \ No newline at end of file diff --git a/vscode-dotnet-runtime-library/src/index.ts b/vscode-dotnet-runtime-library/src/index.ts index cd5af02f4e..5a6f5678ff 100644 --- a/vscode-dotnet-runtime-library/src/index.ts +++ b/vscode-dotnet-runtime-library/src/index.ts @@ -15,6 +15,7 @@ export * from './Acquisition/DotnetResolver'; export * from './Acquisition/ExistingPathResolver'; export * from './Acquisition/GenericDistroSDKProvider'; export * from './Acquisition/GlobalInstallerResolver'; +export * from './Acquisition/GlobalJson'; export * from './Acquisition/IAcquisitionWorkerContext'; export * from './Acquisition/IDotnetConditionValidator'; export * from './Acquisition/IDotnetListInfo'; diff --git a/vscode-dotnet-runtime-library/src/test/unit/VersionUtilities.test.ts b/vscode-dotnet-runtime-library/src/test/unit/VersionUtilities.test.ts index 95104b6831..65eacf07a9 100644 --- a/vscode-dotnet-runtime-library/src/test/unit/VersionUtilities.test.ts +++ b/vscode-dotnet-runtime-library/src/test/unit/VersionUtilities.test.ts @@ -146,4 +146,137 @@ suite('Version Utilities Unit Tests', function () assert.equal(resolver.isValidLongFormVersionFormat(badSDKVersionPeriods, mockEventStream, mockCtx), false, 'It detects a version with a bad number of periods'); }); + test('parseSdkVersion parses SDK versions correctly', async () => + { + const v1 = resolver.parseSdkVersion('8.0.308'); + assert.equal(v1.major, 8); + assert.equal(v1.minor, 0); + assert.equal(v1.featureBand, 3); + assert.equal(v1.patch, 8); + assert.equal(v1.patchFull, 308); + assert.equal(v1.isPrerelease, false); + + const v2 = resolver.parseSdkVersion('9.0.100-preview.1'); + assert.equal(v2.major, 9); + assert.equal(v2.minor, 0); + assert.equal(v2.featureBand, 1); + assert.equal(v2.patch, 0); + assert.equal(v2.isPrerelease, true); + + const v3 = resolver.parseSdkVersion('10.0.200'); + assert.equal(v3.major, 10); + assert.equal(v3.minor, 0); + assert.equal(v3.featureBand, 2); + assert.equal(v3.patch, 0); + }); + + test('isCompatibleSdkVersion with disable policy requires exact match', async () => + { + assert.equal(resolver.isCompatibleSdkVersion('8.0.308', '8.0.308', 'disable'), true); + assert.equal(resolver.isCompatibleSdkVersion('8.0.309', '8.0.308', 'disable'), false); + assert.equal(resolver.isCompatibleSdkVersion('8.0.307', '8.0.308', 'disable'), false); + }); + + test('isCompatibleSdkVersion with patch/latestPatch policy', async () => + { + // Same feature band, higher patch is OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.308', '8.0.305', 'patch'), true); + assert.equal(resolver.isCompatibleSdkVersion('8.0.308', '8.0.305', 'latestPatch'), true); + + // Same feature band, lower patch is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.304', '8.0.305', 'patch'), false); + + // Different feature band is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.400', '8.0.305', 'patch'), false); + + // Different minor is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('8.1.305', '8.0.305', 'patch'), false); + }); + + test('isCompatibleSdkVersion with feature/latestFeature policy', async () => + { + // Higher feature band is OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.400', '8.0.305', 'feature'), true); + assert.equal(resolver.isCompatibleSdkVersion('8.0.400', '8.0.305', 'latestFeature'), true); + + // Same feature band with higher patch is OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.308', '8.0.305', 'feature'), true); + + // Lower feature band is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.200', '8.0.305', 'feature'), false); + + // Different minor is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('8.1.400', '8.0.305', 'feature'), false); + }); + + test('isCompatibleSdkVersion with minor/latestMinor policy', async () => + { + // Higher minor is OK + assert.equal(resolver.isCompatibleSdkVersion('8.1.100', '8.0.305', 'minor'), true); + assert.equal(resolver.isCompatibleSdkVersion('8.1.100', '8.0.305', 'latestMinor'), true); + + // Same minor with higher feature band is OK + assert.equal(resolver.isCompatibleSdkVersion('8.0.400', '8.0.305', 'minor'), true); + + // Different major is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('9.0.100', '8.0.305', 'minor'), false); + }); + + test('isCompatibleSdkVersion with major/latestMajor policy', async () => + { + // Higher major is OK + assert.equal(resolver.isCompatibleSdkVersion('9.0.100', '8.0.305', 'major'), true); + assert.equal(resolver.isCompatibleSdkVersion('9.0.100', '8.0.305', 'latestMajor'), true); + + // Same major with higher minor is OK + assert.equal(resolver.isCompatibleSdkVersion('8.1.100', '8.0.305', 'major'), true); + + // Lower major is NOT OK + assert.equal(resolver.isCompatibleSdkVersion('7.0.400', '8.0.305', 'major'), false); + }); + + test('isCompatibleSdkVersion handles prerelease versions', async () => + { + // Prerelease of same base version is less than release + assert.equal(resolver.isCompatibleSdkVersion('8.0.308-preview.1', '8.0.308', 'disable'), false); + + // Release is compatible with prerelease requirement + assert.equal(resolver.isCompatibleSdkVersion('8.0.308', '8.0.308-preview.1', 'patch'), true); + }); + + test('getCompatibleSdkVersions filters versions correctly', async () => + { + const installed = ['7.0.400', '8.0.304', '8.0.308', '8.0.400', '9.0.100']; + + const patchCompatible = resolver.getCompatibleSdkVersions(installed, '8.0.305', 'patch'); + assert.deepEqual(patchCompatible, ['8.0.308']); + + const featureCompatible = resolver.getCompatibleSdkVersions(installed, '8.0.305', 'feature'); + assert.deepEqual(featureCompatible, ['8.0.308', '8.0.400']); + + const majorCompatible = resolver.getCompatibleSdkVersions(installed, '8.0.305', 'major'); + assert.deepEqual(majorCompatible, ['8.0.308', '8.0.400', '9.0.100']); + }); + + test('compareSdkVersions compares versions correctly', async () => + { + assert.isAbove(resolver.compareSdkVersions('8.0.400', '8.0.308'), 0); + assert.isBelow(resolver.compareSdkVersions('8.0.308', '8.0.400'), 0); + assert.equal(resolver.compareSdkVersions('8.0.308', '8.0.308'), 0); + + // Different major + assert.isAbove(resolver.compareSdkVersions('9.0.100', '8.0.400'), 0); + + // Prerelease is less than release + assert.isBelow(resolver.compareSdkVersions('8.0.308-preview.1', '8.0.308'), 0); + }); + + test('isNewerSdkVersion checks if version is newer', async () => + { + assert.equal(resolver.isNewerSdkVersion('8.0.400', '8.0.308'), true); + assert.equal(resolver.isNewerSdkVersion('8.0.308', '8.0.400'), false); + assert.equal(resolver.isNewerSdkVersion('8.0.308', '8.0.308'), false); + assert.equal(resolver.isNewerSdkVersion('9.0.100', '8.0.400'), true); + }); + }); \ No newline at end of file