blob: c2e8c7bb6d812344416ace6b39e7cca85521adf8 [file] [log] [blame]
// 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 });
}
}