Skip to content

Commit

Permalink
Support bazel tasks from tasks.json (#346)
Browse files Browse the repository at this point in the history
User-facing changes
-------------------

This commit fixes two user-facing bugs:
* The "Run Task" command provides a list of recently used tasks. This
  list also contains the build/test tasks started from the "Bazel Build
  Targets" tree view. However, trying to rerun any of those tasks
  resulted in an error message.
* The extension already contributed a `taskDefinition` for bazel tasks.
  However, trying to actually define such a task in the `tasks.json`
  resulted in the same error message.

In both cases, the user was presented with the error message

> There is no task provider registered for tasks of type "bazel"

This commit fixes the issue, such that rerunning tasks from the history
and configuring tasks through `tasks.json` now works properly.

Furthermore, the `taskDefinition` inside the `package.json` was improved:
* It now lists the available `command` options, such that we have
  auto-completion within the `tasks.json` editor.
* It properly constraints the `targets` entries to be string-typed.
* It exposes the `options` argument which allows to specify additional
  command line parameters.

Code changes
------------

Fixing the issue mainly required registering a `TaskProvider` for
`bazel` tasks. The `TaskProvider` currently does not auto-detect the
available tasks, but only implements `resolveTask`.

The notifications in `onTaskProcessEnd` had to be refactored: So far,
the `BazelTaskInfo` was attached right during task creation. However,
this didn't work for the new `resolveTask`. `resolveTask` apparently
isn't allowed to change a task's definition. To make notifications work,
the `BazelTaskInfo` is now attached in the `onTaskStart` callback.

Many of the fields of the `BazelTaskInfo` were unused. This commit
removes `commandOptions`, `processId`, `exitCode` all of which were
stored but never read. Doing so, it turned out that the
`onTaskProcessStart` was no longer necessary.

Lastly, the callbacks used for task notifications were moved from
`extension.ts` to `bazel_task_info.ts` and activation of all
task-related functionality was centralized in the `activateTaskProvider`
function

Co-authored-by: John Firebaugh <jfirebaugh@figma.com>
  • Loading branch information
vogelsgesang and jfirebaugh authored Mar 1, 2024
1 parent c2f3f8c commit f2426e4
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 91 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This extension provides support for Bazel in Visual Studio.
clicking on the targets
- **Buildifier** integration to lint and format your Bazel files (requires that
[Buildifier](https://github.com/bazelbuild/buildtools/releases) be installed)
- **Bazel Task** definitions for `tasks.json`
- Debug Starlark code in your `.bzl` files during a build (set breakpoints, step
through code, inspect variables, etc.)

Expand Down Expand Up @@ -56,6 +57,27 @@ This extension can use [Facebook's starlark project](https://github.com/facebook
1. Install the LSP using cargo: `cargo install starlark_bin`
2. Enable the LSP extension by setting `bazel.lsp.enabled` to `true`.

## Bazel tasks

Bazel tasks can be configured from the `launch.json` using the following structure:

```json
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Check for flakyness",
"type": "bazel",
"command": "test",
"targets": ["//my/package:integration_test"],
"options": ["--runs_per_test=9"]
}
]
}
```

## Contributing

If you would like to contribute to the Bazel Visual Studio extension, please
Expand Down
21 changes: 19 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,18 +391,35 @@
"taskDefinitions": [
{
"type": "bazel",
"when": "shellExecutionSupported",
"required": [
"command",
"targets"
],
"properties": {
"command": {
"description": "The Bazel command to execute (\"build\" or \"test\").",
"type": "string",
"description": "The Bazel command to execute (\"build\" or \"test\")."
"enum": [
"build",
"test",
"run",
"clean"
]
},
"targets": {
"description": "The labels of the targets to build or test.",
"type": "array",
"items": {
"type": "string"
}
},
"options": {
"description": "Additional command line parameters to pass to Bazel.",
"type": "array",
"description": "The labels of the targets to build or test."
"items": {
"type": "string"
}
}
}
}
Expand Down
59 changes: 38 additions & 21 deletions src/bazel/bazel_task_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,52 @@
// limitations under the License.

import * as vscode from "vscode";
import { IBazelCommandOptions } from "./bazel_command";
import { TASK_TYPE, type BazelTaskDefinition } from "./tasks";
import { exitCodeToUserString, parseExitCode } from "./bazel_exit_code";

/** Information about a Bazel task. */
export class BazelTaskInfo {
/** pid for the task (if started). */
public processId: number;

/** exit code for the task (if completed). */
public exitCode: number;

/** start time (for internal performance tracking). */
public startTime: [number, number];
}

/**
* Initializes a new Bazel task info instance.
*
* @param command The bazel command used (e.g. test, build).
* @param commandOptions The bazel options used.
*/
public constructor(
readonly command: string,
readonly commandOptions: IBazelCommandOptions,
) {}
export function onTaskStart(event: vscode.TaskStartEvent) {
const definition = event.execution.task.definition;
if (definition.type === TASK_TYPE) {
const bazelTaskInfo = new BazelTaskInfo();
bazelTaskInfo.startTime = process.hrtime();
definition.bazelTaskInfo = bazelTaskInfo;
}
}

export function setBazelTaskInfo(task: vscode.Task, info: BazelTaskInfo) {
task.definition.bazelTaskInfo = info;
export function onTaskProcessEnd(event: vscode.TaskProcessEndEvent) {
const task = event.execution.task;
const command = (task.definition as BazelTaskDefinition).command;
const bazelTaskInfo = task.definition.bazelTaskInfo as BazelTaskInfo;
if (bazelTaskInfo) {
const rawExitCode = event.exitCode;

const exitCode = parseExitCode(rawExitCode, command);
if (rawExitCode !== 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showErrorMessage(
`Bazel ${command} failed: ${exitCodeToUserString(exitCode)}`,
);
} else {
const timeInSeconds = measurePerformance(bazelTaskInfo.startTime);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showInformationMessage(
`Bazel ${command} completed successfully in ${timeInSeconds} seconds.`,
);
}
}
}

export function getBazelTaskInfo(task: vscode.Task): BazelTaskInfo {
return task.definition.bazelTaskInfo as BazelTaskInfo;
/**
* Returns the number of seconds elapsed with a single decimal place.
*
*/
function measurePerformance(start: [number, number]) {
const diff = process.hrtime(start);
return (diff[0] + diff[1] / 1e9).toFixed(1);
}
120 changes: 109 additions & 11 deletions src/bazel/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,25 @@
import * as vscode from "vscode";
import { getDefaultBazelExecutablePath } from "../extension/configuration";
import { IBazelCommandOptions } from "./bazel_command";
import { BazelTaskInfo, setBazelTaskInfo } from "./bazel_task_info";
import { onTaskProcessEnd, onTaskStart } from "./bazel_task_info";
import { BazelWorkspaceInfo } from "./bazel_workspace_info";

export const TASK_TYPE = "bazel";

/**
* Definition of a Bazel task
*
* Must be kept in sync with the schema specified in the `taskDefinitions`
* contribution in the `package.json`.
*/
export interface BazelTaskDefinition extends vscode.TaskDefinition {
/** The Bazel command */
command: "build" | "clean" | "test" | "run";
/** The list of Bazel targets */
targets: string[];
/** Additional command line arguments */
options?: string[];
}

/**
* Returns a {@code ShellQuotedString} indicating how to quote the given flag
Expand All @@ -25,16 +43,70 @@ function quotedOption(option: string): vscode.ShellQuotedString {
return { value: option, quoting: vscode.ShellQuoting.Strong };
}

/**
* Task provider for `bazel` tasks.
*/
class BazelTaskProvider implements vscode.TaskProvider {
provideTasks(): vscode.ProviderResult<vscode.Task[]> {
// We don't auto-detect any tasks
return [];
}
async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
// VSCode calls this
// * when rerunning a task from the task history in "Run Task"
// * for bazel tasks in the user's tasks.json,
// We need to inform VSCode how to execute that command by creating
// a ShellExecution for it.

// Infer `BazelWorkspaceInfo` from `scope`
let workspaceInfo: BazelWorkspaceInfo;
if (
task.scope === vscode.TaskScope.Global ||
task.scope === vscode.TaskScope.Workspace
) {
workspaceInfo = await BazelWorkspaceInfo.fromWorkspaceFolders();
} else if (task.scope) {
workspaceInfo = BazelWorkspaceInfo.fromWorkspaceFolder(task.scope);
}
if (!workspaceInfo) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showInformationMessage(
"Please open a Bazel workspace folder to use this task.",
);
return;
}

return createBazelTaskFromDefinition(
task.definition as BazelTaskDefinition,
workspaceInfo,
);
}
}

/**
* Activate support for `bazel` tasks
*/
export function activateTaskProvider(): vscode.Disposable[] {
return [
// Task provider
vscode.tasks.registerTaskProvider(TASK_TYPE, new BazelTaskProvider()),
// Task events
vscode.tasks.onDidStartTask(onTaskStart),
vscode.tasks.onDidEndTaskProcess(onTaskProcessEnd),
];
}

/**
* Creates a new task that invokes a build or test action.
*
* @param command The Bazel command to execute.
* @param options Describes the options used to launch Bazel.
*/
export function createBazelTask(
command: "build" | "clean" | "test" | "run",
options: IBazelCommandOptions,
export function createBazelTaskFromDefinition(
taskDefinition: BazelTaskDefinition,
workspaceInfo: BazelWorkspaceInfo,
): vscode.Task {
const command = taskDefinition.command;
const bazelConfigCmdLine =
vscode.workspace.getConfiguration("bazel.commandLine");
const startupOptions = bazelConfigCmdLine.get<string[]>("startupOptions");
Expand All @@ -46,35 +118,61 @@ export function createBazelTask(
const args = startupOptions
.concat([command as string])
.concat(commandArgs)
.concat(options.targets)
.concat(options.options)
.concat(taskDefinition.targets)
.concat(taskDefinition.options ?? [])
.map(quotedOption);

let commandDescription: string;
let group: vscode.TaskGroup | undefined;
switch (command) {
case "build":
commandDescription = "Build";
group = vscode.TaskGroup.Build;
break;
case "clean":
commandDescription = "Clean";
group = vscode.TaskGroup.Clean;
break;
case "test":
commandDescription = "Test";
group = vscode.TaskGroup.Test;
break;
case "run":
commandDescription = "Run";
break;
}

const targetsDescription = options.targets.join(", ");
const targetsDescription = taskDefinition.targets.join(", ");
const task = new vscode.Task(
{ type: "bazel", command, targets: options.targets },
taskDefinition,
// TODO(allevato): Change Workspace to Global once the fix for
// Microsoft/vscode#63951 is in a stable release.
options.workspaceInfo.workspaceFolder || vscode.TaskScope.Workspace,
workspaceInfo.workspaceFolder || vscode.TaskScope.Workspace,
`${commandDescription} ${targetsDescription}`,
"bazel",
new vscode.ShellExecution(getDefaultBazelExecutablePath(), args, {
cwd: options.workspaceInfo.bazelWorkspacePath,
cwd: workspaceInfo.bazelWorkspacePath,
}),
);
setBazelTaskInfo(task, new BazelTaskInfo(command, options));
task.group = group;
return task;
}

/**
* Creates a new task that invokes a build or test action.
*
* @param command The Bazel command to execute.
* @param options Describes the options used to launch Bazel.
*/
export function createBazelTask(
command: "build" | "clean" | "test" | "run",
options: IBazelCommandOptions,
): vscode.Task {
const taskDefinition: BazelTaskDefinition = {
type: TASK_TYPE,
command,
targets: options.targets,
options: options.options,
};
return createBazelTaskFromDefinition(taskDefinition, options.workspaceInfo);
}
Loading

0 comments on commit f2426e4

Please sign in to comment.