Skip to content

Commit

Permalink
Add bazel.getTargetOutput command (#275)
Browse files Browse the repository at this point in the history
* 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 #273 and could form the basis of #217, #207, and #115.

* Add/update docs

* Lint
  • Loading branch information
jfirebaugh authored Sep 12, 2022
1 parent efbdb1f commit 8039f64
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 84 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"activationEvents": [
"onLanguage:starlark",
"onView:bazelWorkspace",
"onCommand:bazel.refreshBazelBuildTargets"
"onCommand:bazel.refreshBazelBuildTargets",
"onCommand:bazel.getTargetOutput"
],
"main": "./out/src/extension/extension",
"contributes": {
Expand Down Expand Up @@ -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": {
Expand Down
52 changes: 52 additions & 0 deletions src/bazel/bazel_cquery.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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";
}
}
47 changes: 47 additions & 0 deletions src/bazel/bazel_info.ts
Original file line number Diff line number Diff line change
@@ -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 <key>` and returns the output.
*
* @param key The info key to query.
* @returns The output of `bazel info <key>`.
*/
public async run(key: string): Promise<string> {
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";
}
}
76 changes: 35 additions & 41 deletions src/bazel/bazel_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<blaze_query.QueryResult> {
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) => {
Expand All @@ -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<string[]> {
const buffer = await this.run(
additionalOptions.concat(["--output=package"]),
);
public async queryPackages(query: string): Promise<string[]> {
const buffer = await this.run([query, "--output=package"]);
const result = buffer
.toString("utf-8")
.trim()
Expand All @@ -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<Buffer> {
protected run(
options: string[],
{ ignoresErrors = false }: { ignoresErrors?: boolean } = {},
): Promise<Buffer> {
const bazelConfig = vscode.workspace.getConfiguration("bazel");
const queriesShareServer = bazelConfig.get<boolean>("queriesShareServer");
let additionalStartupOptions: string[] = [];
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 2 additions & 6 deletions src/bazel/bazel_quickpick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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));
Expand Down
4 changes: 1 addition & 3 deletions src/bazel/bazel_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
23 changes: 23 additions & 0 deletions src/bazel/bazel_workspace_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 1 addition & 3 deletions src/definition/bazel_goto_definition_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 8039f64

Please sign in to comment.