| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| |
| import * as vscode from 'vscode'; |
| import * as fs from 'fs'; |
| import * as logger from './logger'; |
| import * as path from 'path'; |
| import { Ffx } from './ffx'; |
| |
| const CONFIG_ROOT_NAME = 'fuchsia'; |
| |
| /** |
| * ToolFinder creates and keeps a single Ffx class instance with an up-to-date path to the ffx |
| * binary. |
| */ |
| export class ToolFinder { |
| private ffxPathInternal: string | undefined; |
| private ffxInternal: Ffx; |
| private readonly onDidUpdateFfxEmitter = new vscode.EventEmitter<{ isInitial: boolean }>(); |
| |
| /** |
| * Occurs when the FFX instance is updated. |
| * |
| * Mainly useful for testing |
| */ |
| public readonly onDidUpdateFfx = this.onDidUpdateFfxEmitter.event; |
| |
| constructor() { |
| const cwd = vscode.workspace.workspaceFolders?.map(folder => folder.uri.path)[0]; |
| this.ffxInternal = new Ffx(cwd, undefined, this); |
| this.updateFfxPath(true).catch((err) => logger.error('unable to configure initial ffx location', undefined, err)); |
| vscode.workspace.onDidChangeConfiguration(event => { |
| if (event.affectsConfiguration(`${CONFIG_ROOT_NAME}.ffxPath`)) { |
| this.updateFfxPath(false).catch((err) => logger.error('unable to reconfigure ffx location', undefined, err)); |
| } |
| }); |
| } |
| |
| /** |
| * Get the single Ffx class instance with an up-to-date path to the ffx binary. |
| * @returns Ffx class instance. |
| */ |
| public get ffx(): Ffx { |
| return this.ffxInternal; |
| } |
| |
| /** |
| * For testing: get the path of the ffx binary. |
| * @returns the path to the ffx binary. |
| */ |
| public get ffxPath(): string | undefined { |
| return this.ffxPathInternal; |
| } |
| |
| /** |
| * Returns the full path to the ffx binary from a custom path. The custom path can be relative, |
| * and/or start with `~/` (which means `$HOME`). |
| * @param inputPath custom path to the ffx binary. |
| * @param workspacePath the full path to the workspace. |
| * @returns the full path to the ffx binary. |
| */ |
| resolveAbsolutePath(inputPath: string, workspacePath: string | undefined): string { |
| // NB(sollyross): this mimics what VSCode itself does: |
| // https://github.com/microsoft/vscode/blob/8030b3ac7ace36d062109846b07643a651f4ce51/src/vs/base/common/labels.ts#L200 |
| // Note, specifically, that we only handle `~` and not `~sollyross` or |
| // `~[omg:zsh]` or other shell- and os-specific variants |
| // (which is why `.startsWith('~')` is insufficient) |
| let toolPath = inputPath; |
| if (toolPath.match(/^~($|\/|\\)/) /* are we `~`, `~/` or `~\` */) { |
| if (!process.env.HOME) { |
| throw new Error(`Unable to locate $HOME to expand ~ in tool path. path = "${toolPath}".`); |
| } |
| return path.join(process.env.HOME, toolPath.substring(1)) as string; |
| } else if (path.isAbsolute(toolPath)) { |
| return toolPath; |
| } else { |
| if (workspacePath === undefined) { |
| throw new Error('Could not evaluate the workspace' + |
| `relative path for ffx. path = "${toolPath}".`); |
| } |
| return path.join(workspacePath, toolPath) as string; |
| } |
| } |
| |
| /** |
| * Tests if the ffx path exists and if it's an executable. |
| * |
| * @param ffxPath absolute path to the ffx binary. |
| * |
| * @throws error if this test fails. |
| */ |
| async validateFfxPath(ffxPath: string) { |
| if (!(await fs.promises.stat(ffxPath)).isFile()) { |
| throw new Error(`The path for ffx is not a file. path = "${ffxPath}".`); |
| } |
| // Check if ffx is executable. This has no effect on Windows. |
| await fs.promises.access(ffxPath, fs.constants.X_OK); |
| } |
| |
| // TODO(fxbug.dev/98459): Handle searching multiple workspace folders. |
| /** |
| * If the user has not set the ffx binary path, this method searches for it in known locations |
| * (e.g. tools/ffx), see README.md. |
| * @param workspacePath the full path to the workspace folder |
| * @returns the full path to the ffx binary. |
| * @throws error if the ffx binary path was not found. |
| */ |
| async findFfxPath(workspacePath: string | undefined): Promise<string> { |
| if (workspacePath === undefined) { |
| throw new Error('ffx path error: Cannot read the workspace folder.'); |
| } |
| const ffxPaths = [path.join('.jiri_root', 'bin', 'ffx'), path.join('tools', 'ffx')]; |
| for (const ffxPath of ffxPaths) { |
| const fullFfxPath = path.join(workspacePath, ffxPath); |
| try { |
| await this.validateFfxPath(fullFfxPath); |
| return ffxPath; |
| } catch (err) { |
| if (err instanceof Error) { |
| logger.debug(err.message, 'tool_finder'); |
| } |
| } |
| } |
| throw new Error('Ffx could not be found.'); |
| } |
| |
| /** |
| * Update the ffx binary path. This method is generally called when initializing, or when the |
| * configuration path settings have changed. |
| */ |
| async updateFfxPath(isInitial: boolean) { |
| try { |
| const workspacePath = vscode.workspace.workspaceFolders?.map(folder => folder.uri.path)[0]; |
| const configFfxPath = vscode.workspace.getConfiguration(CONFIG_ROOT_NAME).get('ffxPath'); |
| |
| if (typeof configFfxPath !== 'string') { |
| throw new Error('Could not get the ffxPath configuration.'); |
| } |
| |
| let ffxPath = ''; |
| |
| if (configFfxPath.length === 0) { |
| ffxPath = await this.findFfxPath(workspacePath); |
| // don't update here -- it's unexpected to change the user's configuration for them, and it |
| // makes it hard to adjust for changes in layout automatically in the future. |
| // |
| // Plus, it'd trigger an extra loop-around of updates, meaning an extra target list, |
| // extra messages, etc. |
| } else { |
| ffxPath = configFfxPath; |
| } |
| |
| ffxPath = this.resolveAbsolutePath(ffxPath, workspacePath); |
| await this.validateFfxPath(ffxPath); |
| |
| this.ffxPathInternal = ffxPath; |
| } catch (err) { |
| logger.error('Error updating ffx path', 'tool_finder', err); |
| void vscode.window.showErrorMessage(`Error updating ffx path: ${(err instanceof Error) ? err.message : ''}`); |
| this.ffxPathInternal = undefined; |
| } |
| |
| this.ffxInternal.path = this.ffxPathInternal; |
| if (this.ffxPathInternal) { |
| logger.debug(`FFX path set to '${this.ffxPathInternal}'`); |
| } |
| |
| this.onDidUpdateFfxEmitter.fire({ isInitial }); |
| } |
| } |