xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/bazel.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2024 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15import * as child_process from 'child_process';
16import * as path from 'path';
17
18import * as vscode from 'vscode';
19
20import { getNativeBinary as getBazeliskBinary } from '@bazel/bazelisk';
21import node_modules from 'node_modules-path';
22
23import logger from './logging';
24
25import {
26  bazel_executable,
27  buildifier_executable,
28  settings,
29  ConfigAccessor,
30  bazel_codelens,
31} from './settings';
32
33/**
34 * Is there a path to the given tool configured in VS Code settings?
35 *
36 * @param name The name of the tool
37 * @param configAccessor A config accessor for the setting
38 * @return Whether there is a path in settings that matches
39 */
40function hasConfiguredPathTo(
41  name: string,
42  configAccessor: ConfigAccessor<string>,
43): boolean {
44  const exe = configAccessor.get();
45  return exe
46    ? path.basename(exe).toLowerCase().includes(name.toLowerCase())
47    : false;
48}
49
50/**
51 * Find all system paths to the tool.
52 * @param name The name of the tool
53 * @return List of paths
54 */
55function findPathsTo(name: string): string[] {
56  // TODO: https://pwbug.dev/351883170 - This only works on Unix-ish OSes.
57  try {
58    const stdout = child_process
59      .execSync(`which -a ${name.toLowerCase()}`)
60      .toString();
61    // Parse the output into a list of paths, removing any duplicates/blanks.
62    return [...new Set(stdout.split('\n'))].filter((item) => item.length > 0);
63  } catch (err: unknown) {
64    // If the above finds nothing, it returns a non-zero exit code.
65    return [];
66  }
67}
68
69export function vendoredBazeliskPath(): string | undefined {
70  const result = getBazeliskBinary();
71
72  // If there isn't a binary for this platform, the function appears to return
73  // Promise<1>... strange. Either way, if it's not a string, then we don't
74  // have a path.
75  if (typeof result !== 'string') return undefined;
76
77  return path.resolve(
78    node_modules()!,
79    '@bazel',
80    'bazelisk',
81    path.basename(result),
82  );
83}
84
85/**
86 * Get a path to Bazel no matter what.
87 *
88 * The difference between this and `bazel_executable.get()` is that this will
89 * return the vendored Bazelisk as a last resort, whereas the former only
90 * returns whatever path has been configured.
91 */
92export const getReliableBazelExecutable = () =>
93  bazel_executable.get() ?? vendoredBazeliskPath();
94
95function vendoredBuildifierPath(): string | undefined {
96  const result = getBazeliskBinary();
97
98  // If there isn't a binary for this platform, the function appears to return
99  // Promise<1>... strange. Either way, if it's not a string, then we don't
100  // have a path.
101  if (typeof result !== 'string') return undefined;
102
103  // Unlike the @bazel/bazelisk package, @bazel/buildifer doesn't export any
104  // code. The logic is exactly the same, but with a different name.
105  const binaryName = path.basename(result).replace('bazelisk', 'buildifier');
106
107  return path.resolve(node_modules()!, '@bazel', 'buildifier', binaryName);
108}
109
110const VENDORED_LABEL = 'Use the version built in to the Pigweed extension';
111
112/** Callback called when a tool path is selected from the dropdown. */
113function onSetPathSelection(
114  item: { label: string; picked?: boolean } | undefined,
115  configAccessor: ConfigAccessor<string>,
116  vendoredPath: string,
117) {
118  if (item && !item.picked) {
119    if (item.label === VENDORED_LABEL) {
120      configAccessor.update(vendoredPath);
121    } else {
122      configAccessor.update(item.label);
123    }
124  }
125}
126
127/** Show a checkmark next to the item if it's the current setting. */
128function markIfActive(active: boolean): vscode.ThemeIcon | undefined {
129  return active ? new vscode.ThemeIcon('check') : undefined;
130}
131
132/**
133 * Let the user select a path to the given tool, or use the vendored version.
134 *
135 * @param name The name of the tool
136 * @param configAccessor A config accessor for the setting
137 * @param vendoredPath Path to the vendored version of the tool
138 */
139export async function interactivelySetToolPath(
140  name: string,
141  configAccessor: ConfigAccessor<string>,
142  vendoredPath: string | undefined,
143) {
144  const systemPaths = findPathsTo(name);
145
146  const vendoredItem = vendoredPath
147    ? [
148        {
149          label: VENDORED_LABEL,
150          iconPath: markIfActive(configAccessor.get() === vendoredPath),
151        },
152      ]
153    : [];
154
155  const items = [
156    ...vendoredItem,
157    {
158      label: 'Paths found on your system',
159      kind: vscode.QuickPickItemKind.Separator,
160    },
161    ...systemPaths.map((item) => ({
162      label: item,
163      iconPath: markIfActive(configAccessor.get() === item),
164    })),
165  ];
166
167  if (items.length === 0) {
168    vscode.window.showWarningMessage(
169      `${name} couldn't be found on your system, and we don't have a ` +
170        'vendored version compatible with your system. See the Bazelisk ' +
171        'documentation for installation instructions.',
172    );
173
174    return;
175  }
176
177  vscode.window
178    .showQuickPick(items, {
179      title: `Select a ${name} path`,
180      canPickMany: false,
181    })
182    .then((item) => onSetPathSelection(item, configAccessor, vendoredPath!));
183}
184
185export const interactivelySetBazeliskPath = () =>
186  interactivelySetToolPath(
187    'Bazelisk',
188    bazel_executable,
189    vendoredBazeliskPath(),
190  );
191
192export async function configureBazelisk() {
193  if (settings.disableBazeliskCheck()) return;
194  if (hasConfiguredPathTo('bazelisk', bazel_executable)) return;
195
196  await vscode.window
197    .showInformationMessage(
198      'Pigweed recommends using Bazelisk to manage your Bazel environment. ' +
199        'The Pigweed extension comes with Bazelisk built in, or you can select ' +
200        'an existing Bazelisk install from your system.',
201      'Default',
202      'Select',
203      'Disable',
204    )
205    .then((value) => {
206      switch (value) {
207        case 'Default': {
208          bazel_executable.update(vendoredBazeliskPath());
209          break;
210        }
211        case 'Select': {
212          interactivelySetBazeliskPath();
213          break;
214        }
215        case 'Disable': {
216          settings.disableBazeliskCheck(true);
217          vscode.window.showInformationMessage("Okay, I won't ask again.");
218          break;
219        }
220      }
221    });
222}
223
224export async function setBazelRecommendedSettings() {
225  if (!settings.preserveBazelPath()) {
226    await bazel_executable.update(vendoredBazeliskPath());
227  }
228
229  await buildifier_executable.update(vendoredBuildifierPath());
230  await bazel_codelens.update(true);
231}
232
233export async function configureBazelSettings() {
234  await updateVendoredBazelisk();
235  await updateVendoredBuildifier();
236
237  if (settings.disableBazelSettingsRecommendations()) return;
238
239  await vscode.window
240    .showInformationMessage(
241      "Would you like to use Pigweed's recommended Bazel settings?",
242      'Yes',
243      'No',
244      'Disable',
245    )
246    .then(async (value) => {
247      switch (value) {
248        case 'Yes': {
249          await setBazelRecommendedSettings();
250          await settings.disableBazelSettingsRecommendations(true);
251          break;
252        }
253        case 'Disable': {
254          await settings.disableBazelSettingsRecommendations(true);
255          vscode.window.showInformationMessage("Okay, I won't ask again.");
256          break;
257        }
258      }
259    });
260}
261
262export async function updateVendoredBazelisk() {
263  const isUsingVendoredBazelisk = !!bazel_executable
264    .get()
265    ?.match(/pigweed\.pigweed-.*/);
266
267  if (isUsingVendoredBazelisk && !settings.preserveBazelPath()) {
268    logger.info('Updating Bazelisk path for current extension version');
269    await bazel_executable.update(vendoredBazeliskPath());
270  }
271}
272
273export async function updateVendoredBuildifier() {
274  const isUsingVendoredBuildifier = !!buildifier_executable
275    .get()
276    ?.match(/pigweed\.pigweed-.*/);
277
278  if (isUsingVendoredBuildifier) {
279    logger.info('Updating Buildifier path for current extension version');
280    await buildifier_executable.update(vendoredBuildifierPath());
281  }
282}
283