From 8039f64c530d616deab6b28821221e90bef5c7ce Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 12 Sep 2022 01:14:18 -0700 Subject: [PATCH] Add bazel.getTargetOutput command (#275) * Add bazel.getTargetOutputs command This command can be used in launch configurations to obtain the path to an executable built by Bazel. For example, you can set the "program" attribute of a launch configuration to an input variable: "program": "${input:binaryOutputLocation}" Then define a command input variable: "inputs" [ { "id": "binaryOutputLocation", "type": "command", "command": "bazel.getOutputTarget", "args": ["//my/binary:target"], } ] Addresses https://github.com/bazelbuild/vscode-bazel/issues/273 and could form the basis of https://github.com/bazelbuild/vscode-bazel/issues/217, https://github.com/bazelbuild/vscode-bazel/issues/207, and https://github.com/bazelbuild/vscode-bazel/issues/115. * Add/update docs * Lint --- package.json | 8 +- src/bazel/bazel_cquery.ts | 52 +++++++++++ src/bazel/bazel_info.ts | 47 ++++++++++ src/bazel/bazel_query.ts | 76 ++++++++-------- src/bazel/bazel_quickpick.ts | 8 +- src/bazel/bazel_utils.ts | 4 +- src/bazel/bazel_workspace_info.ts | 23 +++++ .../bazel_goto_definition_provider.ts | 4 +- src/extension/extension.ts | 87 +++++++++++++++---- src/workspace-tree/bazel_package_tree_item.ts | 8 +- .../bazel_workspace_folder_tree_item.ts | 12 ++- 11 files changed, 245 insertions(+), 84 deletions(-) create mode 100644 src/bazel/bazel_cquery.ts create mode 100644 src/bazel/bazel_info.ts diff --git a/package.json b/package.json index 4acc09fb..7615060b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "activationEvents": [ "onLanguage:starlark", "onView:bazelWorkspace", - "onCommand:bazel.refreshBazelBuildTargets" + "onCommand:bazel.refreshBazelBuildTargets", + "onCommand:bazel.getTargetOutput" ], "main": "./out/src/extension/extension", "contributes": { @@ -82,6 +83,11 @@ "category": "Bazel", "command": "bazel.copyTargetToClipboard", "title": "Copy Label to Clipboard" + }, + { + "category": "Bazel", + "command": "bazel.getTargetOutput", + "title": "Get output path for the given target" } ], "configuration": { diff --git a/src/bazel/bazel_cquery.ts b/src/bazel/bazel_cquery.ts new file mode 100644 index 00000000..12a731f7 --- /dev/null +++ b/src/bazel/bazel_cquery.ts @@ -0,0 +1,52 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { BazelQuery } from "./bazel_query"; + +/** Provides a promise-based API around a Bazel cquery. */ +export class BazelCQuery extends BazelQuery { + /** + * Constructs and executes a cquery command that obtains the output files of + * a given target. + * + * @param target The target to query. + * @param options Additional command line options that should be + * passed just to this specific invocation of the query. + * @returns The files that are outputs of the given target, as paths relative + * to the execution root. + */ + public async queryOutputs( + target: string, + options: string[] = [], + ): Promise { + return ( + await this.run([ + target, + ...options, + "--output=starlark", + "--starlark:expr", + '"\\n".join([f.path for f in target.files.to_list()])', + ]) + ) + .toString("utf-8") + .trim() + .replace(/\r\n|\r/g, "\n") + .split("\n") + .sort(); + } + + protected bazelCommand(): string { + return "cquery"; + } +} diff --git a/src/bazel/bazel_info.ts b/src/bazel/bazel_info.ts new file mode 100644 index 00000000..5ef4b29d --- /dev/null +++ b/src/bazel/bazel_info.ts @@ -0,0 +1,47 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as child_process from "child_process"; + +import { BazelCommand } from "./bazel_command"; + +/** Provides a promise-based API around the `bazel info` command. */ +export class BazelInfo extends BazelCommand { + /** + * Runs `bazel info ` and returns the output. + * + * @param key The info key to query. + * @returns The output of `bazel info `. + */ + public async run(key: string): Promise { + return new Promise((resolve, reject) => { + child_process.execFile( + this.bazelExecutable, + this.execArgs([key]), + { cwd: this.workingDirectory }, + (error: Error, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }, + ); + }); + } + + protected bazelCommand(): string { + return "info"; + } +} diff --git a/src/bazel/bazel_query.ts b/src/bazel/bazel_query.ts index 4f65d2ed..7f52f81a 100644 --- a/src/bazel/bazel_query.ts +++ b/src/bazel/bazel_query.ts @@ -24,43 +24,37 @@ import { getBazelWorkspaceFolder } from "./bazel_utils"; /** Provides a promise-based API around a Bazel query. */ export class BazelQuery extends BazelCommand { - /** - * Initializes a new Bazel query. - * - * @param bazelExecutable The path to the Bazel executable. - * @param workingDirectory The path to the directory from which Bazel will be - * spawned. - * @param query The query to execute. - * @param options Command line options that will be passed to Bazel (targets, - * query strings, flags, etc.). - * @param ignoresErrors If true, a non-zero exit code for the child process is - * ignored and the {@link #run} function's promise is resolved with the - * empty string instead. - */ - public constructor( - bazelExecutable: string, - workingDirectory: string, - query: string, - options: string[], - private readonly ignoresErrors: boolean = false, - ) { - super(bazelExecutable, workingDirectory, [query].concat(options)); - } - /** * Runs the query and returns a {@code QueryResult} containing the targets * that match. * - * @param additionalOptions Additional command line options that should be - * passed just to this specific invocation of the query. + * @param query The query to execute. + * @param options + * @param options.additionalOptions Additional command line options that + * should be passed just to this specific invocation of the query. + * @param options.sortByRuleName If `true`, the results from the query will + * be sorted by their name. + * @param options.ignoresErrors `true` if errors from executing the query + * should be ignored. * @returns A {@link QueryResult} object that contains structured information * about the query results. */ public async queryTargets( - additionalOptions: string[] = [], - sortByRuleName: boolean = false, + query: string, + { + additionalOptions = [], + sortByRuleName = false, + ignoresErrors = false, + }: { + additionalOptions?: string[]; + sortByRuleName?: boolean; + ignoresErrors?: boolean; + } = {}, ): Promise { - const buffer = await this.run(additionalOptions.concat(["--output=proto"])); + const buffer = await this.run( + [query, ...additionalOptions, "--output=proto"], + { ignoresErrors }, + ); const result = blaze_query.QueryResult.decode(buffer); if (sortByRuleName) { const sorted = result.target.sort((t1, t2) => { @@ -83,17 +77,12 @@ export class BazelQuery extends BazelCommand { * Runs the query and returns an array of package paths containing the targets * that match. * - * @param additionalOptions Additional command line options that should be - * passed just to this specific invocation of the query. + * @param query The query to execute. * @returns An sorted array of package paths containing the targets that * match. */ - public async queryPackages( - additionalOptions: string[] = [], - ): Promise { - const buffer = await this.run( - additionalOptions.concat(["--output=package"]), - ); + public async queryPackages(query: string): Promise { + const buffer = await this.run([query, "--output=package"]); const result = buffer .toString("utf-8") .trim() @@ -111,12 +100,17 @@ export class BazelQuery extends BazelCommand { * Executes the command and returns a promise for the binary contents of * standard output. * - * @param additionalOptions Additional command line options that apply only to - * this particular invocation of the command. + * @param query The query to execute. + * @param options + * @param options.ignoresErrors `true` if errors from executing the query + * should be ignored. * @returns A promise that is resolved with the contents of the process's * standard output, or rejected if the command fails. */ - private run(additionalOptions: string[] = []): Promise { + protected run( + options: string[], + { ignoresErrors = false }: { ignoresErrors?: boolean } = {}, + ): Promise { const bazelConfig = vscode.workspace.getConfiguration("bazel"); const queriesShareServer = bazelConfig.get("queriesShareServer"); let additionalStartupOptions: string[] = []; @@ -149,11 +143,11 @@ export class BazelQuery extends BazelCommand { }; child_process.execFile( this.bazelExecutable, - this.execArgs(additionalOptions, additionalStartupOptions), + this.execArgs(options, additionalStartupOptions), execOptions, (error: Error, stdout: Buffer, stderr: Buffer) => { if (error) { - if (this.ignoresErrors) { + if (ignoresErrors) { resolve(new Buffer(0)); } else { reject(error); diff --git a/src/bazel/bazel_quickpick.ts b/src/bazel/bazel_quickpick.ts index 6fb44f10..57a999f6 100644 --- a/src/bazel/bazel_quickpick.ts +++ b/src/bazel/bazel_quickpick.ts @@ -77,9 +77,7 @@ async function queryWorkspaceQuickPickTargets( const queryResult = await new BazelQuery( getDefaultBazelExecutablePath(), workspaceInfo.workspaceFolder.uri.fsPath, - query, - [], - ).queryTargets(); + ).queryTargets(query); // Sort the labels so the QuickPick is ordered. const labels = queryResult.target.map((target) => target.rule.name); labels.sort(); @@ -101,9 +99,7 @@ async function queryWorkspaceQuickPickPackages( const packagePaths = await new BazelQuery( getDefaultBazelExecutablePath(), workspaceInfo.workspaceFolder.uri.fsPath, - "...:*", - [], - ).queryPackages(); + ).queryPackages("...:*"); const result: BazelTargetQuickPick[] = []; for (const target of packagePaths) { result.push(new BazelTargetQuickPick("//" + target, workspaceInfo)); diff --git a/src/bazel/bazel_utils.ts b/src/bazel/bazel_utils.ts index 67fd421c..773cbd7b 100644 --- a/src/bazel/bazel_utils.ts +++ b/src/bazel/bazel_utils.ts @@ -47,9 +47,7 @@ export async function getTargetsForBuildFile( const queryResult = await new BazelQuery( bazelExecutable, workspace, - `kind(rule, ${pkg}:all)`, - [], - ).queryTargets([], /* sortByRuleName: */ true); + ).queryTargets(`kind(rule, ${pkg}:all)`, { sortByRuleName: true }); return queryResult; } diff --git a/src/bazel/bazel_workspace_info.ts b/src/bazel/bazel_workspace_info.ts index 8fb67ef5..814be867 100644 --- a/src/bazel/bazel_workspace_info.ts +++ b/src/bazel/bazel_workspace_info.ts @@ -66,6 +66,29 @@ export class BazelWorkspaceInfo { return undefined; } + /** + * Returns a selected Bazel workspace from among the open VS workspace + * folders. If there is only a single workspace folder open, it will be used. + * If there are multiple workspace folders open, a quick-pick window will be + * opened asking the user to choose one. + */ + public static async fromWorkspaceFolders(): Promise< + BazelWorkspaceInfo | undefined + > { + switch (vscode.workspace.workspaceFolders?.length) { + case undefined: + case 0: + return undefined; + case 1: + return this.fromWorkspaceFolder(vscode.workspace.workspaceFolders[0]); + default: + const workspaceFolder = await vscode.window.showWorkspaceFolderPick(); + return workspaceFolder + ? this.fromWorkspaceFolder(workspaceFolder) + : undefined; + } + } + /** * Initializes a new workspace info object. * diff --git a/src/definition/bazel_goto_definition_provider.ts b/src/definition/bazel_goto_definition_provider.ts index 7e7ee6fc..f2ffe2fb 100644 --- a/src/definition/bazel_goto_definition_provider.ts +++ b/src/definition/bazel_goto_definition_provider.ts @@ -54,9 +54,7 @@ export class BazelGotoDefinitionProvider implements DefinitionProvider { const queryResult = await new BazelQuery( getDefaultBazelExecutablePath(), Utils.dirname(document.uri).fsPath, - `kind(rule, "${targetName}")`, - [], - ).queryTargets(); + ).queryTargets(`kind(rule, "${targetName}")`); if (!queryResult.target.length) { return null; diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 8fc9bdf2..da7a27ec 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as path from "path"; import * as vscode from "vscode"; import { @@ -26,6 +27,8 @@ import { IBazelCommandAdapter, parseExitCode, } from "../bazel"; +import { BazelCQuery } from "../bazel/bazel_cquery"; +import { BazelInfo } from "../bazel/bazel_info"; import { BuildifierDiagnosticsManager, BuildifierFormatProvider, @@ -91,6 +94,10 @@ export function activate(context: vscode.ExtensionContext) { "bazel.copyTargetToClipboard", bazelCopyTargetToClipboard, ), + vscode.commands.registerCommand( + "bazel.getTargetOutput", + bazelGetTargetOutput, + ), // CodeLens provider for BUILD files vscode.languages.registerCodeLensProvider( [{ pattern: "**/BUILD" }, { pattern: "**/BUILD.bazel" }], @@ -358,29 +365,17 @@ async function testPackage( * asking the user to choose one. */ async function bazelClean() { - const workspaces = vscode.workspace.workspaceFolders; - let workspaceFolder: vscode.WorkspaceFolder; - - switch (workspaces.length) { - case 0: - vscode.window.showInformationMessage( - "Please open a Bazel workspace folder to use this command.", - ); - return; - case 1: - workspaceFolder = workspaces[0]; - break; - default: - workspaceFolder = await vscode.window.showWorkspaceFolderPick(); - if (workspaceFolder === undefined) { - return; - } + const workspaceInfo = await BazelWorkspaceInfo.fromWorkspaceFolders(); + if (!workspaceInfo) { + vscode.window.showInformationMessage( + "Please open a Bazel workspace folder to use this command.", + ); + return; } - const task = createBazelTask("clean", { options: [], targets: [], - workspaceInfo: BazelWorkspaceInfo.fromWorkspaceFolder(workspaceFolder), + workspaceInfo, }); vscode.tasks.executeTask(task); } @@ -402,6 +397,60 @@ async function bazelCopyTargetToClipboard( vscode.env.clipboard.writeText(target); } +/** + * Get the output of the given target. + * + * If there are multiple outputs, a quick-pick window will be opened asking the + * user to choose one. + * + * The `bazel.getTargetOutput` command can be used in launch configurations to + * obtain the path to an executable built by Bazel. For example, you can set the + * "program" attribute of a launch configuration to an input variable: + * + * "program": "${input:binaryOutputLocation}" + * + * Then define a command input variable: + * + * "inputs" [ + * { + * "id": "binaryOutputLocation", + * "type": "command", + * "command": "bazel.getOutputTarget", + * "args": ["//my/binary:target"], + * } + * ] + */ +async function bazelGetTargetOutput( + target: string, + options: string[] = [], +): Promise { + const workspaceInfo = await BazelWorkspaceInfo.fromWorkspaceFolders(); + if (!workspaceInfo) { + vscode.window.showInformationMessage( + "Please open a Bazel workspace folder to use this command.", + ); + return; + } + const outputPath = await new BazelInfo( + getDefaultBazelExecutablePath(), + workspaceInfo.bazelWorkspacePath, + ).run("output_path"); + const outputs = await new BazelCQuery( + getDefaultBazelExecutablePath(), + workspaceInfo.bazelWorkspacePath, + ).queryOutputs(target, options); + switch (outputs.length) { + case 0: + throw new Error(`Target ${target} has no outputs.`); + case 1: + return path.join(outputPath, "..", outputs[0]); + default: + return await vscode.window.showQuickPick(outputs, { + placeHolder: `Pick an output of ${target}`, + }); + } +} + function onTaskStart(event: vscode.TaskStartEvent) { const bazelTaskInfo = getBazelTaskInfo(event.execution.task); if (bazelTaskInfo) { diff --git a/src/workspace-tree/bazel_package_tree_item.ts b/src/workspace-tree/bazel_package_tree_item.ts index dc33133b..20804074 100644 --- a/src/workspace-tree/bazel_package_tree_item.ts +++ b/src/workspace-tree/bazel_package_tree_item.ts @@ -56,10 +56,10 @@ export class BazelPackageTreeItem const queryResult = await new BazelQuery( getDefaultBazelExecutablePath(), this.workspaceInfo.bazelWorkspacePath, - `//${this.packagePath}:all`, - [], - true, - ).queryTargets([], /* sortByRuleName: */ true); + ).queryTargets(`//${this.packagePath}:all`, { + ignoresErrors: true, + sortByRuleName: true, + }); const targets = queryResult.target.map((target: blaze_query.Target) => { return new BazelTargetTreeItem(this.workspaceInfo, target); }); diff --git a/src/workspace-tree/bazel_workspace_folder_tree_item.ts b/src/workspace-tree/bazel_workspace_folder_tree_item.ts index dd31d1bf..98b903b7 100644 --- a/src/workspace-tree/bazel_workspace_folder_tree_item.ts +++ b/src/workspace-tree/bazel_workspace_folder_tree_item.ts @@ -160,9 +160,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { const packagePaths = await new BazelQuery( getDefaultBazelExecutablePath(), workspacePath, - "...:*", - [], - ).queryPackages(); + ).queryPackages("...:*"); const topLevelItems: BazelPackageTreeItem[] = []; this.buildPackageTree( packagePaths, @@ -177,10 +175,10 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { const queryResult = await new BazelQuery( getDefaultBazelExecutablePath(), workspacePath, - `:all`, - [], - true, - ).queryTargets([], /* sortByRuleName: */ true); + ).queryTargets(`:all`, { + ignoresErrors: true, + sortByRuleName: true, + }); const targets = queryResult.target.map((target: blaze_query.Target) => { return new BazelTargetTreeItem(this.workspaceInfo, target); });