Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 65 additions & 14 deletions vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import
AcquisitionInvoker,
callWithErrorHandling,
CommandExecutor,
compareSdkVersions,
directoryProviderFactory,
DotnetAcquisitionMissingLinuxDependencies,
DotnetAcquisitionRequested,
Expand Down Expand Up @@ -43,11 +44,15 @@ import
ExistingPathResolver,
ExtensionConfigurationWorker,
formatIssueUrl,
getCompatibleSdkVersions,
getInstallIdCustomArchitecture,
getMajor,
getMajorMinor,
getRequirementsFromGlobalJson,
GlobalAcquisitionContextMenuOpened,
GlobalInstallerResolver,
GlobalJson,
GlobalJsonRequirements,
IAcquisitionWorkerContext,
IDotnetAcquireContext,
IDotnetAcquireResult,
Expand All @@ -67,13 +72,19 @@ import
InstallationValidator,
InstallRecord,
InvalidUninstallRequest,
isCompatibleSdkVersion,
isNewerSdkVersion,
IUtilityContext,
JsonInstaller,
LinuxVersionResolver,
LocalInstallUpdateService,
LocalMemoryCacheSingleton,
NoExtensionIdProvided,
ParsedSdkVersion,
parseGlobalJsonContent,
parseSdkVersion,
registerEventStream,
RollForwardPolicy,
UninstallErrorConfiguration,
UserManualInstallFailure,
UserManualInstallRequested,
Expand All @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -841,6 +858,34 @@ ${JSON.stringify(commandContext)}`));
open(url).catch(() => {});
});

const compareSdkVersionsRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.compareSdkVersions}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most important feedback I have here:

I think it's wise for this logic to live here, and we shouldn't reimplement what's done in recommendedVersion and listVersions. Although I think the overhead of the vscode command calls is quite high. The fact that the codebase parses versions as strings instead of a type is definitely a regret and hard to change from the initial design, which is why we haven't replaced it with something like semver. For new code, this makes me think the version parsing/compare could live in CDK instead if there was a smarter type around it, for the sake of not repeating a bad pattern.

If we continue with this, really the best approach would be for some of the version utilities ( and our types) to be put on npm as a library dependency, considering there's no shared caching/persistent data. I have avoided doing that as there's never been a big drive to do so - (although it hurts seeing others have to copy our type files instead of using @types/.)

I understand doing so would:
A - create a new work item
B - create a new maintenance point (which we could hopefully automate the release of alongside new extension releases)
C - delay the progress of your feature

I'm curious on your opinion w.r.t this - it may be best to have it live in CDK for now.

(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<IDotnetAcquireResult | null>
Expand Down Expand Up @@ -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)
Expand Down
147 changes: 147 additions & 0 deletions vscode-dotnet-runtime-library/src/Acquisition/GlobalJson.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
}

/**
* 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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we support more than latestPatch and latestMajor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should store these as static constant strings instead of having them coded here.

}

/**
* 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;
}
Loading
Loading