diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index c421cb21..55ea00a9 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -124,7 +124,11 @@ ], "arduino": { "cli": { - "version": "0.16.0" + "version": { + "owner": "arduino", + "repo": "arduino-cli", + "commitish": "scerza/lib-install-deps" + } } } } diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index 60871e4e..73a437cd 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -1,6 +1,10 @@ import { injectable, postConstruct, inject } from 'inversify'; +import { Message } from '@phosphor/messaging'; +import { addEventListener } from '@theia/core/lib/browser/widgets/widget'; +import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs'; import { LibraryPackage, LibraryService } from '../../common/protocol/library-service'; import { ListWidget } from '../widgets/component-list/list-widget'; +import { Installable } from '../../common/protocol'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; @injectable() @@ -33,4 +37,110 @@ export class LibraryListWidget extends ListWidget { ]); } + protected async install({ item, version }: { item: LibraryPackage, version: Installable.Version }): Promise { + const dependencies = await this.service.listDependencies({ item, version, filterSelf: true }); + let installDependencies: boolean | undefined = undefined; + if (dependencies.length) { + const message = document.createElement('div'); + message.innerHTML = `The library ${item.name}:${version} needs ${dependencies.length === 1 ? 'another dependency' : 'some other dependencies'} currently not installed:`; + const listContainer = document.createElement('div'); + listContainer.style.maxHeight = '300px'; + listContainer.style.overflowY = 'auto'; + const list = document.createElement('ul'); + list.style.listStyleType = 'none'; + for (const { name } of dependencies) { + const listItem = document.createElement('li'); + listItem.textContent = ` - ${name}`; + listItem.style.fontWeight = 'bold'; + list.appendChild(listItem); + } + listContainer.appendChild(list); + message.appendChild(listContainer); + const question = document.createElement('div'); + question.textContent = `Would you like to install ${dependencies.length === 1 ? 'the missing dependency' : 'all the missing dependencies'}?`; + message.appendChild(question); + const result = await new MessageBoxDialog({ + title: `Dependencies for library ${item.name}:${version}`, + message, + buttons: [ + 'Install all', + `Install ${item.name} only`, + 'Cancel' + ], + maxWidth: 740 // Aligned with `settings-dialog.css`. + }).open(); + + if (result) { + const { response } = result; + if (response === 0) { // All + installDependencies = true; + } else if (response === 1) { // Current only + installDependencies = false; + } + } + } + + if (typeof installDependencies === 'boolean') { + return this.service.install({ item, version, installDependencies }); + } + } + +} + +class MessageBoxDialog extends AbstractDialog { + + protected response: number; + + constructor(protected readonly options: MessageBoxDialog.Options) { + super(options); + this.contentNode.appendChild(this.createMessageNode(this.options.message)); + (options.buttons || ['OK']).forEach((text, index) => { + const button = this.createButton(text); + button.classList.add(index === 0 ? 'main' : 'secondary'); + this.controlPanel.appendChild(button); + this.toDisposeOnDetach.push(addEventListener(button, 'click', () => { + this.response = index; + this.accept(); + })); + }); + } + + protected onCloseRequest(message: Message): void { + super.onCloseRequest(message); + this.accept(); + } + + get value(): MessageBoxDialog.Result { + return { response: this.response }; + } + + protected createMessageNode(message: string | HTMLElement): HTMLElement { + if (typeof message === 'string') { + const messageNode = document.createElement('div'); + messageNode.textContent = message; + return messageNode; + } + return message; + } + + protected handleEnter(event: KeyboardEvent): boolean | void { + this.response = 0; + super.handleEnter(event); + } + +} +export namespace MessageBoxDialog { + export interface Options extends DialogProps { + /** + * When empty, `['OK']` will be inferred. + */ + buttons?: string[]; + message: string | HTMLElement; + } + export interface Result { + /** + * The index of `buttons` that was clicked. + */ + readonly response: number; + } } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 5b52720d..ac20dda8 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -81,11 +81,11 @@ export class FilterableListContainer extends React.C } protected async install(item: T, version: Installable.Version): Promise { - const { installable, searchable, itemLabel } = this.props; + const { install, searchable, itemLabel } = this.props; const dialog = new InstallationProgressDialog(itemLabel(item), version); - dialog.open(); try { - await installable.install({ item, version }); + dialog.open(); + await install({ item, version }); const items = await searchable.search({ query: this.state.filterText }); this.setState({ items: this.sort(items) }); } finally { @@ -94,20 +94,20 @@ export class FilterableListContainer extends React.C } protected async uninstall(item: T): Promise { - const uninstall = await new ConfirmDialog({ + const ok = await new ConfirmDialog({ title: 'Uninstall', msg: `Do you want to uninstall ${item.name}?`, ok: 'Yes', cancel: 'No' }).open(); - if (!uninstall) { + if (!ok) { return; } - const { installable, searchable, itemLabel } = this.props; + const { uninstall, searchable, itemLabel } = this.props; const dialog = new UninstallationProgressDialog(itemLabel(item)); - dialog.open(); try { - await installable.uninstall({ item }); + dialog.open(); + await uninstall({ item }); const items = await searchable.search({ query: this.state.filterText }); this.setState({ items: this.sort(items) }); } finally { @@ -121,13 +121,14 @@ export namespace FilterableListContainer { export interface Props { readonly container: ListWidget; - readonly installable: Installable; readonly searchable: Searchable; readonly itemLabel: (item: T) => string; readonly itemRenderer: ListItemRenderer; readonly resolveContainer: (element: HTMLElement) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly filterTextChangeEvent: Event; + readonly install: ({ item, version }: { item: T, version: Installable.Version }) => Promise; + readonly uninstall: ({ item }: { item: T }) => Promise; } export interface State { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index b302ff67..4311f1d4 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -67,6 +67,14 @@ export abstract class ListWidget extends ReactWidget protected onFocusResolved = (element: HTMLElement | undefined) => { this.focusNode = element; + }; + + protected async install({ item, version }: { item: T, version: Installable.Version }): Promise { + return this.options.installable.install({ item, version }); + } + + protected async uninstall({ item }: { item: T }): Promise { + return this.options.installable.uninstall({ item }); } render(): React.ReactNode { @@ -75,7 +83,8 @@ export abstract class ListWidget extends ReactWidget resolveContainer={this.deferredContainer.resolve} resolveFocus={this.onFocusResolved} searchable={this.options.searchable} - installable={this.options.installable} + install={this.install.bind(this)} + uninstall={this.uninstall.bind(this)} itemLabel={this.options.itemLabel} itemRenderer={this.options.itemRenderer} filterTextChangeEvent={this.filterTextChangeEmitter.event} />; diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index efbb37d6..0e37c52e 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -6,6 +6,15 @@ export const LibraryServicePath = '/services/library-service'; export const LibraryService = Symbol('LibraryService'); export interface LibraryService extends Installable, Searchable { list(options: LibraryService.List.Options): Promise; + /** + * When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`. + */ + install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise; + /** + * Set `filterSelf` to `true` if you want to avoid having `item` in the result set. + * Note: as of today (22.02.2021), the CLI works like this: `./arduino-cli lib deps Adaino@0.1.0 ✕ Adaino 0.1.0 must be installed.`. + */ + listDependencies({ item, version, filterSelf }: { item: LibraryPackage, version: Installable.Version, filterSelf?: boolean }): Promise; } export namespace LibraryService { @@ -83,3 +92,9 @@ export namespace LibraryPackage { } } + +export interface LibraryDependency { + readonly name: string; + readonly requiredVersion: Installable.Version; + readonly installedVersion: Installable.Version; +} diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 052e6ee1..a354a487 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -11,12 +11,12 @@ import { PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq } from './cli-protocol/commands/core_pb'; import { BoardDiscovery } from './board-discovery'; -import { CoreClientProvider } from './core-client-provider'; +import { CoreClientAware } from './core-client-provider'; import { BoardDetailsReq, BoardDetailsResp } from './cli-protocol/commands/board_pb'; import { ListProgrammersAvailableForUploadReq, ListProgrammersAvailableForUploadResp } from './cli-protocol/commands/upload_pb'; @injectable() -export class BoardsServiceImpl implements BoardsService { +export class BoardsServiceImpl extends CoreClientAware implements BoardsService { @inject(ILogger) protected logger: ILogger; @@ -25,9 +25,6 @@ export class BoardsServiceImpl implements BoardsService { @named('discovery') protected discoveryLogger: ILogger; - @inject(CoreClientProvider) - protected readonly coreClientProvider: CoreClientProvider; - @inject(OutputService) protected readonly outputService: OutputService; @@ -49,25 +46,6 @@ export class BoardsServiceImpl implements BoardsService { return this.boardDiscovery.getAvailablePorts(); } - private async coreClient(): Promise { - const coreClient = await new Promise(async resolve => { - const client = await this.coreClientProvider.client(); - if (client) { - resolve(client); - return; - } - const toDispose = this.coreClientProvider.onClientReady(async () => { - const client = await this.coreClientProvider.client(); - if (client) { - toDispose.dispose(); - resolve(client); - return; - } - }); - }); - return coreClient; - } - async getBoardDetails(options: { fqbn: string }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; diff --git a/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.d.ts index e87a5de8..1c46ed59 100644 --- a/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.d.ts @@ -76,6 +76,9 @@ export class LibraryInstallReq extends jspb.Message { getVersion(): string; setVersion(value: string): LibraryInstallReq; + getNodeps(): boolean; + setNodeps(value: boolean): LibraryInstallReq; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): LibraryInstallReq.AsObject; @@ -92,6 +95,7 @@ export namespace LibraryInstallReq { instance?: commands_common_pb.Instance.AsObject, name: string, version: string, + nodeps: boolean, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.js b/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.js index 707e4dc4..0dd8b03a 100644 --- a/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/commands/lib_pb.js @@ -965,7 +965,8 @@ proto.cc.arduino.cli.commands.LibraryInstallReq.toObject = function(includeInsta var f, obj = { instance: (f = msg.getInstance()) && commands_common_pb.Instance.toObject(includeInstance, f), name: jspb.Message.getFieldWithDefault(msg, 2, ""), - version: jspb.Message.getFieldWithDefault(msg, 3, "") + version: jspb.Message.getFieldWithDefault(msg, 3, ""), + nodeps: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) }; if (includeInstance) { @@ -1015,6 +1016,10 @@ proto.cc.arduino.cli.commands.LibraryInstallReq.deserializeBinaryFromReader = fu var value = /** @type {string} */ (reader.readString()); msg.setVersion(value); break; + case 4: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setNodeps(value); + break; default: reader.skipField(); break; @@ -1066,6 +1071,13 @@ proto.cc.arduino.cli.commands.LibraryInstallReq.serializeBinaryToWriter = functi f ); } + f = message.getNodeps(); + if (f) { + writer.writeBool( + 4, + f + ); + } }; @@ -1142,6 +1154,24 @@ proto.cc.arduino.cli.commands.LibraryInstallReq.prototype.setVersion = function( }; +/** + * optional bool noDeps = 4; + * @return {boolean} + */ +proto.cc.arduino.cli.commands.LibraryInstallReq.prototype.getNodeps = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.cc.arduino.cli.commands.LibraryInstallReq} returns this + */ +proto.cc.arduino.cli.commands.LibraryInstallReq.prototype.setNodeps = function(value) { + return jspb.Message.setProto3BooleanField(this, 4, value); +}; + + diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 4c90672e..258d1ced 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -167,3 +167,30 @@ export namespace CoreClientProvider { readonly instance: Instance; } } + +@injectable() +export abstract class CoreClientAware { + + @inject(CoreClientProvider) + protected readonly coreClientProvider: CoreClientProvider; + + protected async coreClient(): Promise { + const coreClient = await new Promise(async resolve => { + const client = await this.coreClientProvider.client(); + if (client) { + resolve(client); + return; + } + const toDispose = this.coreClientProvider.onClientReady(async () => { + const client = await this.coreClientProvider.client(); + if (client) { + toDispose.dispose(); + resolve(client); + return; + } + }); + }); + return coreClient; + } + +} diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 2a32258b..bc80f3ff 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -4,7 +4,7 @@ import { relative } from 'path'; import * as jspb from 'google-protobuf'; import { CoreService } from '../common/protocol/core-service'; import { CompileReq, CompileResp } from './cli-protocol/commands/compile_pb'; -import { CoreClientProvider } from './core-client-provider'; +import { CoreClientAware } from './core-client-provider'; import { UploadReq, UploadResp, BurnBootloaderReq, BurnBootloaderResp, UploadUsingProgrammerReq, UploadUsingProgrammerResp } from './cli-protocol/commands/upload_pb'; import { OutputService } from '../common/protocol/output-service'; import { NotificationServiceServer } from '../common/protocol'; @@ -14,10 +14,7 @@ import { firstToUpperCase, firstToLowerCase } from '../common/utils'; import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; @injectable() -export class CoreServiceImpl implements CoreService { - - @inject(CoreClientProvider) - protected readonly coreClientProvider: CoreClientProvider; +export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(OutputService) protected readonly outputService: OutputService; @@ -152,25 +149,6 @@ export class CoreServiceImpl implements CoreService { } } - private async coreClient(): Promise { - const coreClient = await new Promise(async resolve => { - const client = await this.coreClientProvider.client(); - if (client) { - resolve(client); - return; - } - const toDispose = this.coreClientProvider.onClientReady(async () => { - const client = await this.coreClientProvider.client(); - if (client) { - toDispose.dispose(); - resolve(client); - return; - } - }); - }); - return coreClient; - } - private mergeSourceOverrides(req: { getSourceOverrideMap(): jspb.Map }, options: CoreService.Compile.Options): void { const sketchPath = FileUri.fsPath(options.sketchUri); for (const uri of Object.keys(options.sourceOverride)) { diff --git a/arduino-ide-extension/src/node/library-service-server-impl.ts b/arduino-ide-extension/src/node/library-service-server-impl.ts index 2b6db3e6..bdafaf9f 100644 --- a/arduino-ide-extension/src/node/library-service-server-impl.ts +++ b/arduino-ide-extension/src/node/library-service-server-impl.ts @@ -1,6 +1,6 @@ -import { injectable, inject, postConstruct } from 'inversify'; -import { LibraryPackage, LibraryService } from '../common/protocol/library-service'; -import { CoreClientProvider } from './core-client-provider'; +import { injectable, inject } from 'inversify'; +import { LibraryDependency, LibraryPackage, LibraryService } from '../common/protocol/library-service'; +import { CoreClientAware } from './core-client-provider'; import { LibrarySearchReq, LibrarySearchResp, @@ -12,48 +12,28 @@ import { LibraryInstallResp, LibraryUninstallReq, LibraryUninstallResp, - Library + Library, + LibraryResolveDependenciesReq } from './cli-protocol/commands/lib_pb'; import { Installable } from '../common/protocol/installable'; import { ILogger, notEmpty } from '@theia/core'; -import { Deferred } from '@theia/core/lib/common/promise-util'; import { FileUri } from '@theia/core/lib/node'; import { OutputService, NotificationServiceServer } from '../common/protocol'; @injectable() -export class LibraryServiceImpl implements LibraryService { +export class LibraryServiceImpl extends CoreClientAware implements LibraryService { @inject(ILogger) protected logger: ILogger; - @inject(CoreClientProvider) - protected readonly coreClientProvider: CoreClientProvider; - @inject(OutputService) protected readonly outputService: OutputService; @inject(NotificationServiceServer) protected readonly notificationServer: NotificationServiceServer; - protected ready = new Deferred(); - - @postConstruct() - protected init(): void { - this.coreClientProvider.client().then(client => { - if (client) { - this.ready.resolve(); - } else { - this.coreClientProvider.onClientReady(() => this.ready.resolve()); - } - }) - } - async search(options: { query?: string }): Promise { - await this.ready.promise; - const coreClient = await this.coreClientProvider.client(); - if (!coreClient) { - return []; - } + const coreClient = await this.coreClient(); const { client, instance } = coreClient; const listReq = new LibraryListReq(); @@ -96,12 +76,7 @@ export class LibraryServiceImpl implements LibraryService { } async list({ fqbn }: { fqbn?: string | undefined }): Promise { - await this.ready.promise; - const coreClient = await this.coreClientProvider.client(); - if (!coreClient) { - return []; - } - + const coreClient = await this.coreClient(); const { client, instance } = coreClient; const req = new LibraryListReq(); req.setInstance(instance); @@ -157,20 +132,42 @@ export class LibraryServiceImpl implements LibraryService { }).filter(notEmpty); } - async install(options: { item: LibraryPackage, version?: Installable.Version }): Promise { - await this.ready.promise; + async listDependencies({ item, version, filterSelf }: { item: LibraryPackage, version: Installable.Version, filterSelf?: boolean }): Promise { + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + const req = new LibraryResolveDependenciesReq(); + req.setInstance(instance); + req.setName(item.name); + req.setVersion(version); + const dependencies = await new Promise((resolve, reject) => { + client.libraryResolveDependencies(req, (error, resp) => { + if (error) { + reject(error); + return; + } + resolve(resp.getDependenciesList().map(dep => { + name: dep.getName(), + installedVersion: dep.getVersioninstalled(), + requiredVersion: dep.getVersionrequired() + })); + }) + }); + return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies; + } + + async install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise { const item = options.item; const version = !!options.version ? options.version : item.availableVersions[0]; - const coreClient = await this.coreClientProvider.client(); - if (!coreClient) { - return; - } + const coreClient = await this.coreClient(); const { client, instance } = coreClient; const req = new LibraryInstallReq(); req.setInstance(instance); req.setName(item.name); req.setVersion(version); + if (options.installDependencies === false) { + req.setNodeps(true); + } console.info('>>> Starting library package installation...', item); const resp = client.libraryInstall(req); @@ -193,10 +190,7 @@ export class LibraryServiceImpl implements LibraryService { async uninstall(options: { item: LibraryPackage }): Promise { const item = options.item; - const coreClient = await this.coreClientProvider.client(); - if (!coreClient) { - return; - } + const coreClient = await this.coreClient(); const { client, instance } = coreClient; const req = new LibraryUninstallReq();