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