Permit Installing .NET via a package.json file (#1976)

* Permit Installing .NET via a package.json file

Extensions that rely on .NET often have a slow startup time due to our existing API.

For extensions that want the .NET runtime to be installed as soon as possible, they can now also make an API request in their package.json.

They should add a [IDotnetAcquireContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts) object in a section titled 'x-dotnet-acquire' to do so. They don't need to include the `requestingExtensionId`, since this is already in their `package.json`,

This should go at the root of a `package.json`, and not in the `contributes` section.
```
"x-dotnet-acquire": {
    "version": "8.0",
    "mode": "aspnetcore"
}
```

We run the install on startup and also whenever a new extension is installed via the onDidChange API. https://code.visualstudio.com/api/references/vscode-api#Extension<T>

You can easily test this by modifying the SDK extension and adding this, then packging t with `vsce`, installing it, and running the Runtime Extension under the Debug tab. If you do so, you will see that:

If you install a new extension, the request is made.

If you had already installed the extension, the request is also made.

* Respond to linter

* Add a very basic test.

* Fix typo

* Fix typo
This commit is contained in:
Noah Gilson 2024-10-04 09:22:04 -07:00 коммит произвёл GitHub
Родитель 40d1a8bc66
Коммит 2a876ace5d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 187 добавлений и 11 удалений

Просмотреть файл

@ -4,7 +4,11 @@ This article outlines the commands exposed by the .NET Install Tool. To see thes
## dotnet.acquire
This command will install a .NET runtime. It accepts a [IDotnetAcquireContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts) object and returns a [IDotnetAcquireResult](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireResult.ts), which contains the path to the .NET runtime executable. Note that the version value in IDotnetAcquireContext must be a valid major.minor runtime version, for example 3.1. The extension will automatically identify and install the latest patch of the provided version. It is generally recommended that extension authors call this command immedietly on every extension start up to ensure that the .NET runtime has been installed and is ready to use.
This command will install a .NET runtime. It accepts a [IDotnetAcquireContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts) object and returns a [IDotnetAcquireResult](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireResult.ts), which contains the path to the .NET runtime executable. Note that the version value in IDotnetAcquireContext must be a valid major.minor runtime version, for example 3.1. The extension will automatically identify and install the latest patch of the provided version. It is generally recommended that extension authors call this command immediately on every extension start up to ensure that the .NET runtime has been installed and is ready to use.
## dotnet.acquireGlobalSDK
This command will install a .NET SDK globally. It accepts a [IDotnetAcquireContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts) object and returns a [IDotnetAcquireResult](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireResult.ts).
## dotnet.showAcquisitionLog
@ -13,3 +17,34 @@ This command surfaces an output channel to the user which provides status messag
## dotnet.ensureDotnetDependencies
This command is only applicable to linux machines. It attempts to ensure that .NET dependencies are present and, if they are not, installs them or prompts the user to do so. It accepts a [IDotnetEnsureDependenciesContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetEnsureDependenciesContext.ts) object and has a void return type.
## dotnet.findPath
You can execute this command to return a string to a dotnet runtime path. Pass it a [IDotnetFindPathContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetFindPathContext.ts) object. The major.minor will be respected based on the requirement you give (greater_than_or_equal will return a dotnet on the PATH that is >= the version requested.)
This returns undefined if no matches are found.
## dotnet.uninstall
You can execute this command to dereference / uninstall .NET, either the SDK or runtime, as long as it's managed by this extension. Pass it a [IDotnetFindPathContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetFindPathContext.ts) object suggesting which install you want to remove.
.NET will only be completely uninstalled if all extensions that relied on that version of .NET asked for it to be uninstalled.
Note that users can manually uninstall any version of .NET if they so chose and accept the risk.
# JSON Installation
If you want the .NET runtime to be installed as soon as possible, you can also make an API request in your package.json.
Add a [IDotnetAcquireContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts) object in a section titled 'x-dotnet-acquire' to do so. You don't need to include the `requestingExtensionId`.
This should go at the root of your `package.json`, and not in the `contributes` section.
When any extension is changed, or on startup, we will try to fulfill these requests.
The disadvantage to this approach is you cannot respond to a failure to install, check the status before making the request,
lookup the PATH first to see if there's a matching install, etc.
```json
"x-dotnet-acquire": {
"version": "8.0",
"mode": "aspnetcore"
}
```

Просмотреть файл

@ -2,7 +2,8 @@
[![Version](https://img.shields.io/visual-studio-marketplace/v/ms-dotnettools.vscode-dotnet-runtime?style=for-the-badge)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime) [![Installs](https://img.shields.io/visual-studio-marketplace/i/ms-dotnettools.vscode-dotnet-runtime?style=for-the-badge)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime)
This extension provides a unified way for other extensions like the [C#] and [C# Dev Kit] extensions to install local versions of the .NET Runtime, and machine-wide versions of the .NET SDK. Those extensions tell the .NET Install Tool when they would like a .NET SDK to be on the machine, and we install one for them if there's not already one that matches the SDK they need to run properly. In the future, this tool may support allowing users to call the API via VS Code to install .NET and pick a version of the SDK to install themselves.
This extension provides a unified way for other extensions like the [C#] and [C# Dev Kit] extensions to install local versions of the .NET Runtime, and machine-wide versions of the .NET SDK. Those extensions tell the .NET Install Tool when they would like a .NET SDK to be on the machine, and we install one for them if there's not already one that matches the SDK they need to run properly. Users can also install the .NET SDK themselves by reading below.
## Why do I have this extension?
@ -17,10 +18,9 @@ This extension was probably included as a dependency of one of the following ext
The above extensions call into this extension to provide a unified way of downloading shared .NET Runtimes or .NET SDKs. If you already have an installation of .NET that you'd like to use, see [the troubleshooting section below](#i-already-have-a-net-runtime-or-sdk-installed-and-i-want-to-use-it). If you want to remove this extension completely, you will need to uninstall any extensions that depend on it first. If this extension is uninstalled, any .NET Runtimes installed by it will also be removed.
## [Preview] Using the extension yourself
## Using the extension yourself
As of version 2.0.2, you can install the .NET SDK using part of our private API via the VS Code Command Palette!
This feature is in preview and still undergoing testing.
To use the feature:
Bring up the command palette (ctrl + shift + p) and run the command:
@ -29,7 +29,7 @@ Bring up the command palette (ctrl + shift + p) and run the command:
![Video demonstrating use of the command pallet to install .NET.](https://raw.githubusercontent.com/dotnet/vscode-dotnet-runtime/63b7fca6c714781dc4cb1cdbcb786013f2115098/Documentation/example.gif)
The command will try to find the best version of .NET for you to install, but you can tell it to install other versions as well based on its prompt.
Note this feature is in preview, and does not support all distros, WSL, nor preview or RC versions of .NET.
Note this feature does not support all distros, WSL, nor preview or RC versions of .NET.
The rest of the extension functionality is still limited to other extensions that rely on our extension.

Просмотреть файл

@ -76,6 +76,7 @@ import {
DotnetFindPathDidNotMeetCondition,
DotnetFindPathMetCondition,
DotnetFindPathCommandInvoked,
JsonInstaller,
} from 'vscode-dotnet-runtime-library';
import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId';
import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acquisition/InstallTrackerSingleton';
@ -757,6 +758,9 @@ We will try to install .NET, but are unlikely to be able to connect to the serve
return null;
}
// Preemptively install .NET for extensions who tell us to in their package.json
const jsonInstaller = new JsonInstaller(globalEventStream, vsCodeExtensionContext);
// Exposing API Endpoints
vsCodeContext.subscriptions.push(
dotnetAcquireRegistration,

Просмотреть файл

@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*--------------------------------------------------------------------------------------------*/
import { IEventStream } from '../EventStream/EventStream';
import { IVSCodeExtensionContext } from '../IVSCodeExtensionContext';
export abstract class IJsonInstaller
{
constructor(protected readonly eventStream: IEventStream, protected readonly vscodeAccessor : IVSCodeExtensionContext) {}
public abstract executeJSONRequests(): Promise<void>;
}

Просмотреть файл

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*--------------------------------------------------------------------------------------------*/
import { IEventStream } from "../EventStream/EventStream";
import { DotnetVSCodeExtensionChange, DotnetVSCodeExtensionFound, DotnetVSCodeExtensionHasInstallRequest } from "../EventStream/EventStreamEvents";
import { IDotnetAcquireContext } from "../IDotnetAcquireContext";
import { IVSCodeExtensionContext } from "../IVSCodeExtensionContext";
import { IJsonInstaller } from "./IJsonInstaller";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
export class JsonInstaller extends IJsonInstaller
{
constructor(protected readonly eventStream: IEventStream, protected readonly vscodeAccessor : IVSCodeExtensionContext)
{
super(eventStream, vscodeAccessor);
// If a new extension is installed, we want to install .NET preemptively for it if specified
vscodeAccessor.registerOnExtensionChange(() =>
{
this.eventStream.post(new DotnetVSCodeExtensionChange(`A change was detected in the extensions. Installing .NET for new extensions.`));
this.executeJSONRequests().catch( () => {});
})
// On startup, (our extension gets activated onStartupFinished() via 'activationEvents' in package.json) we want to install .NET preemptively
// So other extensions can have a faster startup time if they so desire
this.executeJSONRequests().catch( () => {});
}
// eslint-disable-next-line @typescript-eslint/require-await
public async executeJSONRequests(): Promise<void>
{
const extensions = this.vscodeAccessor.getExtensions();
for(const extension of extensions)
{
const extensionPackage = extension?.packageJSON;
this.eventStream.post(new DotnetVSCodeExtensionFound(`Checking extension ${extension?.id} for .NET installation requests`));
if(extensionPackage['x-dotnet-acquire'])
{
this.eventStream.post(new DotnetVSCodeExtensionHasInstallRequest(`Installing .NET for extension ${extension.id}`));
const jsonRequest = (extensionPackage as { "x-dotnet-acquire": Omit<IDotnetAcquireContext, "requestingExtensionId"> })["x-dotnet-acquire"];
const apiRequest : IDotnetAcquireContext = { ...jsonRequest, requestingExtensionId: extension.id };
this.vscodeAccessor.executeCommand('dotnet.acquire', apiRequest);
}
}
}
}

Просмотреть файл

@ -876,6 +876,18 @@ export class DotnetTelemetrySettingEvent extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetTelemetrySettingEvent';
}
export class DotnetVSCodeExtensionFound extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetVSCodeExtensionFound';
}
export class DotnetVSCodeExtensionHasInstallRequest extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetVSCodeExtensionHasInstallRequest';
}
export class DotnetVSCodeExtensionChange extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetVSCodeExtensionChange';
}
export class DotnetCommandNotFoundEvent extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetCommandNotFoundEvent';
}

Просмотреть файл

@ -8,4 +8,10 @@ export abstract class IVSCodeExtensionContext
abstract setVSCodeEnvironmentVariable(variable : string, value : string) : void;
abstract appendToEnvironmentVariable(variable : string, pathAdditionWithDelimiter : string) : void;
abstract registerOnExtensionChange<A extends any[], R>(f: (...args: A) => R, ...args: A) : void;
abstract getExtensions() : readonly any[];
abstract executeCommand(command : string, ...args: any[]) : Thenable<any>;
}

Просмотреть файл

@ -22,9 +22,26 @@ export class VSCodeExtensionContext extends IVSCodeExtensionContext
environment.replace(variable, value);
}
appendToEnvironmentVariable(variable: string, appendingValue: string): void
public appendToEnvironmentVariable(variable: string, appendingValue: string): void
{
const environment = this.context.environmentVariableCollection;
environment?.append(variable, appendingValue);
}
public registerOnExtensionChange<A extends any[], R>(f: (...args: A) => R, ...args: A): void
{
vscode.extensions.onDidChange(() => {
f(...(args));
})
}
public getExtensions() : readonly vscode.Extension<any>[]
{
return vscode.extensions.all;
}
public executeCommand(command : string, ...args: any[]) : Thenable<any>
{
return vscode.commands.executeCommand(command, ...args);
}
}

Просмотреть файл

@ -39,6 +39,8 @@ export * from './Acquisition/DotnetCoreAcquisitionWorker';
export * from './Acquisition/DotnetConditionValidator';
export * from './Acquisition/IDotnetPathFinder';
export * from './Acquisition/DotnetPathFinder';
export * from './Acquisition/IJsonInstaller';
export * from './Acquisition/JsonInstaller';
export * from './Acquisition/IDotnetConditionValidator';
export * from './Acquisition/IDotnetListInfo';
export * from './Acquisition/DotnetInstall';

Просмотреть файл

@ -226,6 +226,19 @@ export class MockIndexWebRequestWorker extends WebRequestWorker {
export class MockVSCodeExtensionContext extends IVSCodeExtensionContext
{
registerOnExtensionChange<A extends any[], R>(f: (...args: A) => R, ...args: A): void {
f(...args);
}
getExtensions(): readonly any[]
{
return [{ extensionId : 'test'}];
}
executeCommand(command: string, ...args: any[]): Thenable<any> {
return Promise.resolve({});
}
appendToEnvironmentVariable(variable: string, pathAdditionWithDelimiter: string): void {
// Do nothing.
}

Просмотреть файл

@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*--------------------------------------------------------------------------------------------*/
import * as chai from 'chai';
import { JsonInstaller } from '../../Acquisition/JsonInstaller';
import { MockEventStream, MockVSCodeExtensionContext } from '../mocks/MockObjects';
import { DotnetVSCodeExtensionFound } from '../../EventStream/EventStreamEvents';
const assert = chai.assert;
suite('JSONInstaller Unit Tests', () => {
const eventStream = new MockEventStream();
const mockContext = new MockVSCodeExtensionContext();
test('It Scans Extensions Without x-dotnet-acquire', async () =>
{
const _ = new JsonInstaller(eventStream,mockContext);
const acquireEvent = eventStream.events.find(event => event instanceof DotnetVSCodeExtensionFound) as DotnetVSCodeExtensionFound;
assert.exists(acquireEvent, 'The extensions were scanned, and did not cause an error with having an empty json value')
});
});

Двоичные данные
vscode-dotnet-sdk-extension/images/dotnetIcon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

8
vscode-dotnet-sdk-extension/package-lock.json сгенерированный
Просмотреть файл

@ -56,6 +56,7 @@
"@types/vscode": "1.74.0",
"@vscode/extension-telemetry": "^0.9.7",
"@vscode/sudo-prompt": "^9.3.1",
"@vscode/test-electron": "^2.4.1",
"axios": "^1.7.4",
"axios-cache-interceptor": "^1.5.3",
"axios-retry": "^3.4.0",
@ -71,8 +72,7 @@
"run-script-os": "^1.1.6",
"semver": "^7.6.2",
"shelljs": "^0.8.5",
"typescript": "^5.5.4",
"vscode-test": "^1.6.1"
"typescript": "^5.5.4"
},
"devDependencies": {
"@types/chai": "4.2.22",
@ -6383,6 +6383,7 @@
"@types/vscode": "1.74.0",
"@vscode/extension-telemetry": "^0.9.7",
"@vscode/sudo-prompt": "^9.3.1",
"@vscode/test-electron": "^2.4.1",
"axios": "^1.7.4",
"axios-cache-interceptor": "^1.5.3",
"axios-retry": "^3.4.0",
@ -6400,8 +6401,7 @@
"run-script-os": "^1.1.6",
"semver": "^7.6.2",
"shelljs": "^0.8.5",
"typescript": "^5.5.4",
"vscode-test": "^1.6.1"
"typescript": "^5.5.4"
}
},
"watchpack": {

Просмотреть файл

@ -2188,6 +2188,7 @@ util-deprecate@~1.0.1:
"@types/vscode" "1.74.0"
"@vscode/extension-telemetry" "^0.9.7"
"@vscode/sudo-prompt" "^9.3.1"
"@vscode/test-electron" "^2.4.1"
axios "^1.7.4"
axios-cache-interceptor "^1.5.3"
axios-retry "^3.4.0"
@ -2204,7 +2205,6 @@ util-deprecate@~1.0.1:
semver "^7.6.2"
shelljs "^0.8.5"
typescript "^5.5.4"
vscode-test "^1.6.1"
optionalDependencies:
fsevents "^2.3.3"