Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
"bazel.buildifierExecutable": {
"type": "string",
"default": "",
"markdownDescription": "The name of the Buildifier executable. This may be an absolute path, or a simple name that will be searched for on the system path. Paths starting with `@` are interpreted as Bazel targets and are run via `bazel run`. If empty, \"buildifier\" on the system path will be used.\n\nBuildifier can be downloaded from https://github.com/bazelbuild/buildtools/releases.",
"markdownDescription": "The name of the Buildifier executable. This may be an absolute path, or a simple name that will be searched for on the system path. Paths starting with `@` are interpreted as Bazel targets and are run via `bazel run`. If the value matches a Buildifier release (e.g. `v8.2.1`) this will be downloaded (for the current platform) and used. If empty, \"buildifier\" on the system path will be used.\n\nBuildifier can be downloaded from https://github.com/bazelbuild/buildtools/releases.",
"scope": "machine-overridable"
},
"bazel.buildifierConfigJsonPath": {
Expand Down
48 changes: 20 additions & 28 deletions src/buildifier/buildifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import * as util from "util";
import * as vscode from "vscode";

import { IBuildifierResult, IBuildifierWarning } from "./buildifier_result";
import { getDefaultBazelExecutablePath } from "../extension/configuration";
import { extensionContext } from "../extension/extension";

const execFile = util.promisify(child_process.execFile);
type PromiseExecFileException = child_process.ExecFileException & {
Expand Down Expand Up @@ -164,25 +164,6 @@ export function getBuildifierFileType(fsPath: string): BuildifierFileType {
return "default";
}

/**
* Gets the path to the buildifier executable specified by the workspace
* configuration, if present.
*
* @returns The path to the buildifier executable specified in the workspace
* configuration, or just "buildifier" if not present (in which case the
* system path will be searched).
*/
export function getDefaultBuildifierExecutablePath(): string {
// Try to retrieve the executable from VS Code's settings. If it's not set,
// just use "buildifier" as the default and get it from the system PATH.
const bazelConfig = vscode.workspace.getConfiguration("bazel");
const buildifierExecutable = bazelConfig.get<string>("buildifierExecutable");
if (buildifierExecutable.length === 0) {
return "buildifier";
}
return buildifierExecutable;
}

/**
* Gets the path to the buildifier json configuration file specified by the
* workspace configuration, if present.
Expand All @@ -195,6 +176,15 @@ export function getDefaultBuildifierJsonConfigPath(): string {
return bazelConfig.get<string>("buildifierConfigJsonPath", "");
}

/** A description of an executable and the arguments to pass to it. */
export interface IExecutable {
/** The path to the executable. */
path: string;

/** The arguments that should be passed to the executable. */
args: string[];
}

/**
* Executes buildifier with the given file content and arguments.
*
Expand All @@ -210,16 +200,18 @@ export async function executeBuildifier(
acceptNonSevereErrors: boolean,
): Promise<{ stdout: string; stderr: string }> {
// Determine the executable
let executable = getDefaultBuildifierExecutablePath();
const state = extensionContext.workspaceState.get<IExecutable>(
"buildifierExecutable",
);
if (state !== undefined) {
return Promise.reject("No buildifier executable set.");
}
const { path: executable, args: execArgs } = state;
args = execArgs.concat(args);

const buildifierConfigJsonPath = getDefaultBuildifierJsonConfigPath();
if (buildifierConfigJsonPath.length !== 0) {
args = ["--config", buildifierConfigJsonPath, ...args];
}
// Paths starting with an `@` are referring to Bazel targets
if (executable.startsWith("@")) {
const targetName = executable;
executable = getDefaultBazelExecutablePath();
args = ["run", targetName, "--", ...args];
args.push("--config", buildifierConfigJsonPath);
}
const execOptions = {
maxBuffer: Number.MAX_SAFE_INTEGER,
Expand Down
100 changes: 64 additions & 36 deletions src/buildifier/buildifier_availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,77 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as fs from "fs/promises";
import * as path from "path";
import * as fs from "fs";
import * as vscode from "vscode";
import * as which from "which";

import { executeBuildifier, IExecutable } from "./buildifier";
import { getDefaultBazelExecutablePath } from "../extension/configuration";
import {
executeBuildifier,
getDefaultBuildifierExecutablePath,
} from "./buildifier";

async function fileExists(filename: string) {
try {
await fs.stat(filename);
return true;
} catch {
return false;
}
}
downloadBuildifier,
getBuildifierExecutablePath,
} from "./buildifier_downloader";
import { extensionContext } from "../extension/extension";

/** The URL to load for buildifier's releases. */
const BUILDTOOLS_RELEASES_URL =
"https://github.com/bazelbuild/buildtools/releases";

/**
* Gets the buildifier configuration.
*
* @returns The buildifier configuration.
*/
export function getBuildifierConfiguration(): string {
const bazelConfig = vscode.workspace.getConfiguration("bazel");
return bazelConfig.get<string>("buildifierExecutable", "buildifier");
}

/**
* Returns the path to the buildifier executable and arguments to use.
*
* This is the central point for resolving the buildifier executable. It may
* involve downloading a buildifier release.
*
* @returns The path to the buildifier executable and arguments to use.
*/
async function getBuildifierExecutable(): Promise<IExecutable> {
const configValue = getBuildifierConfiguration();
// Bazel target
if (configValue.startsWith("@")) {
return {
path: getDefaultBazelExecutablePath(),
args: ["run", configValue, "--"],
};
}
// Absolute path
if (fs.existsSync(configValue)) {
return { path: configValue, args: [] };
}
// File on $PATH
try {
return { path: await which(configValue), args: [] };
} catch (e) {
// nothing on PATH
}
// Release binary
try {
const dst = getBuildifierExecutablePath(configValue);
if (fs.existsSync(dst.fsPath)) {
return { path: dst.fsPath, args: [] };
}
return {
path: (await downloadBuildifier(configValue)).fsPath,
args: [],
};
} catch (e) {
// Can't download a release with that version
}
await showBuildifierDownloadPrompt(
`Did not find a buildifier for "${configValue}"`,
);
}

/**
* Checks whether buildifier is available (either at the system PATH or a
* user-specified path, depending on the value in Settings).
Expand All @@ -43,28 +91,8 @@ const BUILDTOOLS_RELEASES_URL =
* Download button that they can use to go to the GitHub releases page.
*/
export async function checkBuildifierIsAvailable() {
const buildifierExecutable = getDefaultBuildifierExecutablePath();

// Check if the program exists (in case it's an actual executable and not
// an target name starting with `@`).
const isTarget = buildifierExecutable.startsWith("@");

// Check if the program exists as a relative path of the workspace
const pathExists = await fileExists(
path.join(
vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath,
buildifierExecutable,
),
);

if (!isTarget && !pathExists) {
try {
await which(buildifierExecutable);
} catch (e) {
await showBuildifierDownloadPrompt("Buildifier was not found");
return;
}
}
const state = await getBuildifierExecutable();
await extensionContext.workspaceState.update("buildifierExecutable", state);

// Make sure it's a compatible version by running
// buildifier on an empty input and see if it exits successfully and the
Expand Down
119 changes: 119 additions & 0 deletions src/buildifier/buildifier_downloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2025 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 fs from "fs";
import * as os from "os";
import { pipeline } from "stream/promises";
import * as vscode from "vscode";
import { extensionContext } from "../extension/extension";

/**
* Returns the expected name of the buildifier executable for the current
* platform.
* @returns The name of the buildifier executable.
*/
function getBuildifierExecutableName(): string {
const platform = os.platform();
const arch = os.arch();

let name: string;
switch (platform) {
case "darwin":
name =
arch === "arm64"
? "buildifier-darwin-arm64"
: "buildifier-darwin-amd64";
break;
case "linux":
name =
arch === "arm64" ? "buildifier-linux-arm64" : "buildifier-linux-amd64";
break;
case "win32":
name = "buildifier-windows-amd64.exe";
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
return name;
}

function getBuildifierExecutableDir(version: string): vscode.Uri {
return vscode.Uri.joinPath(
extensionContext.globalStorageUri,
"buildifier",
version,
);
}

export function getBuildifierExecutablePath(version: string): vscode.Uri {
return vscode.Uri.joinPath(
getBuildifierExecutableDir(version),
getBuildifierExecutableName(),
);
}

/**
* Downloads the buildifier executable for the current platform and the given
* version.
* @param version The version of buildifier to download.
* @returns The path to the downloaded executable.
*/
export async function downloadBuildifier(version: string): Promise<vscode.Uri> {
const exe = getBuildifierExecutableName();
const url = `https://github.com/bazelbuild/buildtools/releases/download/${version}/${exe}`;
const dir = getBuildifierExecutableDir(version);
// example: '/Users/laurenz/Library/Application Support/Code/User/globalStorage/bazelbuild.vscode-bazel/buildifier/v8.2.1/buildifier-darwin-arm64'
const dst = getBuildifierExecutablePath(version);

if (!fs.existsSync(dir.fsPath)) {
fs.mkdirSync(dir.fsPath, { recursive: true });
}

// Show progress notification while downloading the buildifier executable
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: `Downloading buildifier ${version}...`,
cancellable: false,
},
async () => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to download buildifier from ${url}: ${response.statusText}`,
);
}
if (!response.body) {
throw new Error(`Response body of ${url} is empty.`);
}

const fileStream = fs.createWriteStream(dst.fsPath);
await pipeline(response.body as any, fileStream);

makeExecutable(dst);
},
);

return dst;
}

/**
* Makes the given file executable.
* @param uri The URI of the file to make executable.
*/
function makeExecutable(uri: vscode.Uri) {
if (os.platform() !== "win32") {
fs.chmodSync(uri.fsPath, 0o755);
}
}
11 changes: 11 additions & 0 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ import { activateCommandVariables } from "./command_variables";
import { activateTesting } from "../test-explorer";
import { activateWrapperCommands } from "./bazel_wrapper_commands";

export let extensionContext: vscode.ExtensionContext;

/**
* Called when the extension is activated; that is, when its first command is
* executed.
*
* @param context The extension context.
*/
export async function activate(context: vscode.ExtensionContext) {
extensionContext = context;

const workspaceTreeProvider =
BazelWorkspaceTreeProvider.fromExtensionContext(context);
context.subscriptions.push(workspaceTreeProvider);
Expand Down Expand Up @@ -157,6 +161,13 @@ export async function activate(context: vscode.ExtensionContext) {
...activateCommandVariables(),
// Test provider
...activateTesting(),

// Listen for configuration changes that affect buildifier
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration("bazel.buildifierExecutable")) {
await checkBuildifierIsAvailable();
}
}),
);

// Notify the user if buildifier is not available on their path (or where
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"outDir": "out",
"lib": [
"es2022",
"dom"
],
"sourceMap": true,
"rootDir": ".",
Expand Down