diff --git a/client/package-lock.json b/client/package-lock.json index 6a51bda8..01ff6476 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,12 +12,14 @@ "src/lib" ], "dependencies": { + "ejs": "^3.1.10", "find": "^0.3.0", "node-pty": "^1.0.0", "semver": "^7.6.3", "vscode-languageclient": "^9.0.1" }, "devDependencies": { + "@types/ejs": "^3.1.5", "@types/find": "^0.2.4", "@types/semver": "^7.5.8", "@types/vscode": "^1.96.0" @@ -55,6 +57,12 @@ "node": "*" } }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "node_modules/@types/find": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@types/find/-/find-0.2.4.tgz", @@ -73,6 +81,25 @@ "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", "dev": true }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -84,6 +111,64 @@ "balanced-match": "^1.0.0" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, "node_modules/find": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", @@ -92,6 +177,51 @@ "traverse-chain": "~0.1.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimatch": { "version": "5.1.6", "license": "ISC", @@ -127,6 +257,17 @@ "node": ">=10" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index fc2c934e..2dd84b66 100644 --- a/client/package.json +++ b/client/package.json @@ -25,12 +25,14 @@ "vscode": "^1.92.0" }, "dependencies": { + "ejs": "^3.1.10", "find": "^0.3.0", "node-pty": "^1.0.0", "semver": "^7.6.3", "vscode-languageclient": "^9.0.1" }, "devDependencies": { + "@types/ejs": "^3.1.5", "@types/find": "^0.2.4", "@types/semver": "^7.5.8", "@types/vscode": "^1.96.0" diff --git a/client/src/extension.ts b/client/src/extension.ts index 32be5f3d..498e8ea3 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -28,6 +28,7 @@ import { type BitbakeScanResult, scanContainsData } from './lib/src/types/Bitbak import { reviewDiagnostics } from './language/diagnosticsSupport' import { embeddedLanguageDocsManager } from './language/EmbeddedLanguageDocsManager' import { NotificationMethod } from './lib/src/types/notifications' +import { DependsDotView } from './ui/DependsDotView' let client: LanguageClient const bitbakeDriver: BitbakeDriver = new BitbakeDriver() @@ -37,6 +38,7 @@ const bitbakeWorkspace: BitbakeWorkspace = new BitbakeWorkspace() let bitbakeRecipesView: BitbakeRecipesView | undefined let devtoolWorkspacesView: DevtoolWorkspacesView | undefined let terminalProvider: BitbakeTerminalProfileProvider | undefined +let dependsDotWebview: DependsDotView | undefined export function canModifyConfig (): boolean { const disableConfigModification = vscode.workspace.getConfiguration('bitbake').get('disableConfigModification') @@ -160,6 +162,8 @@ export async function activate (context: vscode.ExtensionContext): Promise bitbakeRecipesView.registerView(context) devtoolWorkspacesView = new DevtoolWorkspacesView(bitBakeProjectScanner) devtoolWorkspacesView.registerView(context) + dependsDotWebview = new DependsDotView(bitBakeProjectScanner, context.extensionUri) + dependsDotWebview.registerView(context) void vscode.commands.executeCommand('setContext', 'bitbake.active', true) const bitbakeStatusBar = new BitbakeStatusBar(bitBakeProjectScanner) context.subscriptions.push(bitbakeStatusBar.statusBarItem) diff --git a/client/src/ui/BitbakeCommands.ts b/client/src/ui/BitbakeCommands.ts index ef06c134..09473da0 100644 --- a/client/src/ui/BitbakeCommands.ts +++ b/client/src/ui/BitbakeCommands.ts @@ -54,6 +54,8 @@ export function registerBitbakeCommands (context: vscode.ExtensionContext, bitba vscode.commands.registerCommand('bitbake.stop-toaster', async () => { await stopToaster(bitBakeProjectScanner.bitbakeDriver) }), vscode.commands.registerCommand('bitbake.clear-workspace-state', async () => { await clearAllWorkspaceState(context) }), vscode.commands.registerCommand('bitbake.examine-dependency-taskexp', async (uri) => { await examineDependenciesTaskexp(bitbakeWorkspace, bitBakeProjectScanner, uri) }), + // Repurpose to be per recipe? (and ask for image if not already supplied) + vscode.commands.registerCommand('bitbake.oe-depends-dot', async (uri) => { await runOeDependsDot(bitbakeWorkspace, bitBakeProjectScanner, uri) }), // Handles enqueued parsing requests (onSave) vscode.tasks.onDidEndTask((e) => { if (e.execution.task.name === 'Bitbake: Parse') { @@ -615,6 +617,30 @@ export async function examineDependenciesTaskexp (bitbakeWorkspace: BitbakeWorks } } +export async function runOeDependsDot (bitbakeWorkspace: BitbakeWorkspace, bitBakeProjectScanner: BitBakeProjectScanner, uri?: unknown): Promise { + if (isTaskexpStarted) { + void vscode.window.showInformationMessage('taskexp is already started') + return + } + const chosenImage = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) + const chosenRecipe = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) + if (chosenImage !== undefined && chosenRecipe !== undefined) { + logger.debug(`Command: oe-depends-dot: ${chosenRecipe}`) + isTaskexpStarted = true + const process = await runBitbakeTerminal(bitBakeProjectScanner.bitbakeDriver, + { + specialCommand: `bitbake -g ${chosenRecipe} -u taskexp` + } as BitbakeTaskDefinition, + `Bitbake: taskexp: ${chosenRecipe}`) + process.onExit((e) => { + isTaskexpStarted = false + if (e.exitCode !== 0) { + void vscode.window.showErrorMessage(`Failed to start taskexp with exit code ${e.exitCode}. See terminal output.`) + } + }) + } +} + async function openBitbakeDevshell (terminalProvider: BitbakeTerminalProfileProvider, bitbakeWorkspace: BitbakeWorkspace, bitBakeProjectScanner: BitBakeProjectScanner, uri?: unknown): Promise { const chosenRecipe = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) if (chosenRecipe === undefined) return diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts new file mode 100644 index 00000000..6f16f8e6 --- /dev/null +++ b/client/src/ui/DependsDotView.ts @@ -0,0 +1,215 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode' +import ejs from 'ejs' +import { BitbakeDriver } from '../driver/BitbakeDriver' +import { logger } from '../lib/src/utils/OutputLogger' +import { BitbakeTaskDefinition } from './BitbakeTaskProvider' +import { runBitbakeTerminal } from './BitbakeTerminal' +import { finishProcessExecution } from '../utils/ProcessUtils' +import { BitBakeProjectScanner } from '../driver/BitBakeProjectScanner' +import { ElementInfo } from '../lib/src/types/BitbakeScanResult' +import path from 'path' + +/* + TODO Beautify the view + - make div elements side by side + - Add some spacing + TODO Display a graph rather than text + TODO Make the graph interactive (click on elements open their .bb file) (bonus: right click brings the commands menu) + TODO Auto-refresh the dotfile when needed when click dependsDot + TODO display a checkbox wether the graph is up-to-date, successful or failed + TODO gray out the results when the graph or package is not up-to-date + TODO Use the select recipe command to get the image recipe list (not for the packageName though?) + TODO Add tests for this feature + TODO Save field values on workspace reload (https://code.visualstudio.com/api/extension-guides/webview#getstate-and-setstate and serializer) + TODO test styling in white mode and high-contrast mode + TODO sanitize text input (server side) + TODO add a gif in the README for this feature +*/ + +export class DependsDotView { + private readonly provider: DependsDotViewProvider + + constructor (bitbakeProjectScanner: BitBakeProjectScanner, extensionUri: vscode.Uri) { + this.provider = new DependsDotViewProvider(bitbakeProjectScanner, extensionUri) + } + + registerView (context: vscode.ExtensionContext): void { + context.subscriptions.push(vscode.window.registerWebviewViewProvider( + DependsDotViewProvider.viewType, + this.provider)) + } +} + +class DependsDotViewProvider implements vscode.WebviewViewProvider { + private readonly bitbakeDriver: BitbakeDriver + private readonly bitbakeProjectScanner: BitBakeProjectScanner + public static readonly viewType = "bitbake.oeDependsDot" + private view?: vscode.WebviewView; + private extensionUri: vscode.Uri + + private depType: string = "-w"; + private graphRecipe: string = ""; + private packageName: string = ""; + + constructor (bitbakeProjectScanner: BitBakeProjectScanner, extensionUri: vscode.Uri) { + this.bitbakeDriver = bitbakeProjectScanner.bitbakeDriver + this.bitbakeProjectScanner = bitbakeProjectScanner + this.extensionUri = extensionUri + } + + async resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): Promise { + this.view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + + localResourceRoots: [ + this.extensionUri + ] + }; + + webviewView.webview.html = await this.getHtmlForWebview(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(this.onWebviewMessage.bind(this)); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private onWebviewMessage(data: any) : any { + switch (data.type) { + case 'depType': + this.depType = data.value === "depends" ? "-d" : "-w"; + break; + case 'graphRecipe': + this.graphRecipe = data.value; + break; + case 'packageName': + this.packageName = data.value; + break; + case 'genDotFile': + this.genDotFile(); + break; + case 'runOeDepends': + this.runOeDepends(); + break; + case 'openRecipe': + this.openRecipe(data.value); + break; + } + } + + private getHtmlForWebview(webview: vscode.Webview): Promise { + const htmlUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.html')); + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.js')); + const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'reset.css')); + const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'vscode.css')); + const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.css')); + + const nonce = this.getNonce(); + + const html = ejs.renderFile(htmlUri.fsPath, { + nonce: nonce, + scriptUri: scriptUri, + styleResetUri: styleResetUri, + styleVSCodeUri: styleVSCodeUri, + styleMainUri: styleMainUri, + webview: webview + }); + return html; + } + + /// The Nonce is a random value used to validate the CSP policy + private getNonce() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } + + private async genDotFile() : Promise { + if(this.graphRecipe === "") { + logger.error("genDotFile: No image recipe selected"); + void vscode.window.showErrorMessage(`Please select an image recipe first`); + return; + } + logger.info(`genDotFile: ${this.graphRecipe}`) + // isTaskexpStarted = true + // TODO add blocker for runOeDependsDOt to wait for completion. + // TODO do not bring to foreground + const process = await runBitbakeTerminal(this.bitbakeDriver, + { + specialCommand: `bitbake -g ${this.graphRecipe}`, + } as BitbakeTaskDefinition, + `Bitbake: genDotFile: ${this.graphRecipe}`,) + process.onExit((e) => { + // isTaskexpStarted = false + if (e.exitCode !== 0) { + void vscode.window.showErrorMessage(`Failed to generate dependency graph with exit code ${e.exitCode}. See terminal output.`) + } + }) + } + + private async runOeDepends() : Promise { + if(this.packageName === "") { + logger.error("genDotFile: No package selected"); + void vscode.window.showErrorMessage(`Please select a package first`); + return; + } + logger.info(`runOeDepends: ${this.packageName}`); + // TODO do not bring to foreground + const process = runBitbakeTerminal(this.bitbakeDriver, + { + specialCommand: `oe-depends-dot -k ${this.packageName} ${this.depType} ./task-depends.dot`, + } as BitbakeTaskDefinition, + `Bitbake: oeDependsDot: ${this.packageName}`,) + const result = await finishProcessExecution(process) + if (result.status !== 0) { + void vscode.window.showErrorMessage(`Failed to run oe-depends-dot with exit code ${result.status}. See terminal output.`) + } + const filtered_output = this.filterOeDependsOutput(result.stdout.toString()); + this.view?.webview.postMessage({ type: 'results', value: filtered_output, depType: this.depType }); + } + + /// Remove all lines of output that do not contain the actual results + private filterOeDependsOutput(output: string): string { + let filtered_output = '' + if(this.depType === "-d") { + filtered_output = output + .split('\n') + .filter(line => line.includes('Depends: ')) + .map(line => line.replace('Depends: ', '')) + .join('\n'); + } else { + filtered_output = output + .split('\n') + .filter(line => line.includes(' -> ')) + .join('\n'); + } + return filtered_output; + } + + private openRecipe(recipeName: string) { + recipeName = recipeName.replace(/\r/g, ''); + let recipeFound = false; + this.bitbakeProjectScanner.scanResult._recipes.forEach((recipe: ElementInfo) => { + // TODO fix resolving -native recipes (put that logic in a utility function) (could be shared with BitbakeRecipesView.getChildren) + // TODO fix resolving some packages like xz or busybox (only when in the bottom row?) + if (recipe.name === recipeName) { + if (recipe.path !== undefined) { + vscode.window.showTextDocument(vscode.Uri.file(path.format(recipe.path))); + recipeFound = true; + } + } + }) + if (!recipeFound) { + vscode.window.showErrorMessage(`Project scan was not able to resolve ${recipeName}`); + } + } +} diff --git a/client/web/depends-dot/main.css b/client/web/depends-dot/main.css new file mode 100644 index 00000000..96b7ba5f --- /dev/null +++ b/client/web/depends-dot/main.css @@ -0,0 +1,11 @@ +/* More variables in https://code.visualstudio.com/api/references/theme-color */ + +body { + background-color: transparent; +} + +.packageLine:hover { + color: var(--vscode-list-hoverForeground); + background: var(--vscode-list-hoverBackground); + cursor: pointer; +} diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html new file mode 100644 index 00000000..587c3d2b --- /dev/null +++ b/client/web/depends-dot/main.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + OE Depends Dot + + + + +
+

Select an image Recipe to analyze:

+ + +
+ +
+

You want to know:

+
    +
  • Why a package is included

  • +
  • What it Depends on

  • +
+
+ +
+

Select which Package you want to examine:

+ +
+ +
+ +
+ +
+

Results:

+
+
+
+ + + + diff --git a/client/web/depends-dot/main.js b/client/web/depends-dot/main.js new file mode 100644 index 00000000..20d0c32a --- /dev/null +++ b/client/web/depends-dot/main.js @@ -0,0 +1,106 @@ +/** -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +//@ts-check + +// This script will be run within the webview itself +// It cannot access the main VS Code APIs directly. +(function () { + const vscode = acquireVsCodeApi(); + const oldState = vscode.getState() || { }; + + // HTML Listeners + document.querySelector('#depType').addEventListener('click', (event) => { + vscode.postMessage({ type: 'depType', value: event.target.value }); + }); + + document.querySelector('#graphRecipe').addEventListener('input', () => { + const value = document.querySelector('#graphRecipe').value; + vscode.postMessage({ type: 'graphRecipe', value }); + }); + + document.querySelector('#packageName').addEventListener('input', () => { + const value = document.querySelector('#packageName').value; + vscode.postMessage({ type: 'packageName', value }); + }); + + document.querySelector('#genDotFile').addEventListener('click', () => { + vscode.postMessage({ type: 'genDotFile' }); + }); + + document.querySelector('#runOeDepends').addEventListener('click', () => { + vscode.postMessage({ type: 'runOeDepends' }); + }); + + // Extension Listeners + window.addEventListener('message', event => { + const message = event.data; // The json data that the extension sent + // TODO remove empty final line, especially when asking for an inexistent package + switch (message.type) { + case 'results': + { + if(message.depType === '-w') { + renderWhy(message, vscode); + } else { + renderDependencies(message, vscode); + } + break; + } + } + }); +}()); + +function renderDependencies(message, vscode) { + const resultsDiv = document.querySelector('#results'); + resultsDiv.innerHTML = ''; + + const packages = message.value.split(' '); + packages.forEach(pkg => { + addPackageLine(resultsDiv, pkg, '•', vscode); + }); +} + +function renderWhy(message, vscode) { + const resultsDiv = document.querySelector('#results'); + resultsDiv.innerHTML = ''; + + const dependencyChains = message.value.split('\n'); + for(let i = 0; i < dependencyChains.length; i++) { + const chain = dependencyChains[i]; + const chainDiv = document.createElement('div'); + chainDiv.className = 'dependencyChain'; + resultsDiv.appendChild(chainDiv); + renderDependencyChain(chain, chainDiv, vscode); + } +} + +function renderDependencyChain(chain, element, vscode) { + // Use the unicode box drawing characters to draw the lines + // https://www.compart.com/en/unicode/block/U+2500 + const packages = chain.split(' -> '); + for(let i = 0; i < packages.length; i++) { + const pkg = packages[i]; + let icon = '┃'; + if(i === 0) { icon = '┳'; } + if(i === packages.length - 1) { icon = '┻'; } + addPackageLine(element, pkg, icon, vscode); + } +} + +function addPackageLine(element, name, graphIcon, vscode) { + const div = document.createElement('div'); + div.className = 'packageLine'; + div.innerHTML = `${graphIcon} ${name}`; + element.appendChild(div); + div.addEventListener('click', () => { + vscode.postMessage({ type: 'openRecipe', value: name }); + }); +} + +function addIconLine(element, icon) { + const div = document.createElement('div'); + div.className = 'iconLine'; + div.innerHTML = `${icon}`; + element.appendChild(div); +} diff --git a/client/web/depends-dot/reset.css b/client/web/depends-dot/reset.css new file mode 100644 index 00000000..717b7cbe --- /dev/null +++ b/client/web/depends-dot/reset.css @@ -0,0 +1,31 @@ +/* From https://github.com/microsoft/vscode-extension-samples/blob/5ddd30fc052e03bbec52e5d84627eaa543fb0de8/webview-view-sample/media/vscode.css under MIT license */ +html { + box-sizing: border-box; + font-size: 13px; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +ol, +ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +img { + max-width: 100%; + height: auto; +} diff --git a/client/web/depends-dot/vscode.css b/client/web/depends-dot/vscode.css new file mode 100644 index 00000000..082e24ec --- /dev/null +++ b/client/web/depends-dot/vscode.css @@ -0,0 +1,92 @@ +/* From https://github.com/microsoft/vscode-extension-samples/blob/5ddd30fc052e03bbec52e5d84627eaa543fb0de8/webview-view-sample/media/vscode.css under MIT license */ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +body { + padding: 0 var(--container-paddding); + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: var(--vscode-editor-background); +} + +ol, +ul { + padding-left: var(--container-paddding); +} + +body > *, +form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} + +button { + border: none; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + width: 100%; + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); +} + +button:hover { + cursor: pointer; + background: var(--vscode-button-hoverBackground); +} + +button:focus { + outline-color: var(--vscode-focusBorder); +} + +button.secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); +} + +button.secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 872fb82a..dd613aeb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,7 +31,7 @@ export default [ { pattern: " \\* Copyright \\(c\\) .*\\. All rights reserved\\.", template: - " * Copyright (c) 2023 Savoir-faire Linux. All rights reserved.", + " * Copyright (c) 2025 Savoir-faire Linux. All rights reserved.", }, " * Licensed under the MIT License. See License.txt in the project root for license information.", " * ------------------------------------------------------------------------------------------ ", diff --git a/package.json b/package.json index e0beacc7..50d664c0 100644 --- a/package.json +++ b/package.json @@ -467,6 +467,11 @@ "title": "BitBake: Examine recipe's dependencies with taskexp", "description": "Examine the recipe's dependencies with taskexp." }, + { + "command": "bitbake.oe-depends-dot", + "title": "BitBake: Generate a dependency graph with oe-depends-dot", + "description": "Generate a dependency graph with oe-depends-dot" + }, { "command": "bitbake.devtool-modify", "title": "BitBake: Devtool: Modify recipe", @@ -559,6 +564,14 @@ "contextualTitle": "Devtool workspaces", "icon": "$(symbol-property)", "when": "bitbake.active" + }, + { + "type": "webview", + "id": "bitbake.oeDependsDot", + "name": "Dependency Analyzer", + "contextualTitle": "Dependency Analyzer", + "icon": "$(graph)", + "when": "bitbake.active" } ] }, @@ -626,6 +639,10 @@ "command": "bitbake.pick-configuration", "group": "1@bitbake_dev@6" }, + { + "command": "bitbake.oe-depends-dot", + "group": "1@bitbake_dev@7" + }, { "command": "bitbake.devtool-modify", "group": "2@bitbake_devtool@0" @@ -818,6 +835,11 @@ "group": "1@bitbake_dev@4", "when": "viewItem == bitbakeRecipeCtx" }, + { + "command": "bitbake.oe-depends-dot", + "group": "1@bitbake_dev@5", + "when": "viewItem == bitbakeRecipeCtx" + }, { "command": "bitbake.devtool-modify", "group": "2@bitbake_devtool@0",