blob: 60cf7647a55211b352a405e3db6f74061ef0f8bc [file] [log] [blame] [edit]
// Copyright 2025 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 { window } from 'vscode';
import { Setup } from './extension';
import * as logger from './logger';
import { registerCommandWithAnalyticsEvent } from './analytics/vscode_events';
import { FuchsiaDiagnostics } from './problem_matcher';
export const BUILD_MATCHER = /^\[.*?\(\d+\)/;
const PRODUCT_PLACEHOLDERS = new Map([
['bringup', 'Minimal viable development target'],
['core', 'Starting point for higher-level product configurations'],
['minimal', 'Smallest thing which can be called Fuchsia'],
['workbench_eng', 'Not consumer-oriented, to explore Fuchsia']
]);
enum CompilationModes {
debug = '',
release = '--release',
balanced = '--balanced'
};
export interface Packages {
title: string,
value: Array<string>,
notes: string
}
interface Build {
product: string,
board: string,
compilation: string,
packages: Array<Packages>
}
// TODO(fxbug.dev/390002761) Add vscode state tracking
// TODO(fxbug.dev/390002761) side-panel with more complex build configs
/**
* initialize fuchsia workflow interaction commands
*/
export function setUpWorkflowInteraction(ctx: vscode.ExtensionContext, setup: Setup) {
let collection = new FuchsiaDiagnostics();
/** Registers fx.set with analytics */
registerCommandWithAnalyticsEvent('fuchsia.fx.set',
async (product?: string,
board?: string,
compilation?: string,
packages?: Array<vscode.QuickPickItem>
) => {
let status: Build = {
product: '',
board: '',
compilation: '',
packages: [],
};
await getStatus(status);
if (!product) {
await setup.fx.runFx(['list-products']).then(async (data) => {
const list = toList(data, status.product, PRODUCT_PLACEHOLDERS);
const selected = await window.showQuickPick(list, {
title: 'Select Product'
});
product = selected?.label;
});
}
if (!board) {
await setup.fx.runFx(['list-boards']).then(async (data) => {
const list = toList(data, status.board);
const selected = await window.showQuickPick(list, {
title: 'Select Board'
});
board = selected?.label;
});
}
if (!compilation) {
const list = toList(Object.keys(CompilationModes), status.compilation);
const selected = await window.showQuickPick(list, {
title: 'Select Compilation Mode'
});
compilation = selected?.label;
}
if (status.packages?.length > 0) {
const list = packageToList(status.packages);
const selected = await window.showQuickPick(list, {
title: 'Select Packages',
canPickMany: true
});
packages = selected;
}
if (!product || !board || !compilation) {
return;
}
if (didArgsChange()) {
return;
}
runFuchsiaSet(getArgs());
/** Helper that checks fx set args did change */
function didArgsChange(): Boolean {
let mode = status.compilation as keyof typeof CompilationModes;
let lastSet = `${status.product}.${status.board} ${CompilationModes[mode]}`;
mode = compilation as keyof typeof CompilationModes;
let set = `${product}.${board} ${CompilationModes[mode]}`;
return lastSet === set ? true : false;
}
/** Helper to convert args into array of strings */
function getArgs(): Array<string> {
let args = [
'set',
`${product?.trimEnd()}.${board?.trimEnd()}`,
CompilationModes[compilation as keyof typeof CompilationModes]
];
packages?.forEach((pkg) => {
if (pkg.picked) {
args.push(`${pkg.description}`);
args.push(pkg.label);
}
});
args = args.filter(Boolean);
return args;
}
/** Get current target configuration from `fx status`*/
async function getStatus(status: Build) {
const process = setup.fx.runJsonStreaming(['status', '--format=json'], (data: any) => {
const buildInfo = data.buildInfo.items;
for (let item in buildInfo) {
switch (item) {
case 'boards':
status.board = buildInfo[item].value;
break;
case 'products':
status.product = buildInfo[item].value;
break;
case 'compilation_mode':
status.compilation = buildInfo[item].value;
break;
default:
status.packages.push(buildInfo[item]);
}
}
});
await process.exitCode;
}
/** Run fx set */
function runFuchsiaSet(args: string[]) {
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'fx set',
cancellable: true
}, async (progress, token) => {
let cmd = setup.fx.runAsync(
args,
(buffer) => {
let msg = new TextDecoder().decode(buffer);
logger.info(msg, 'fx set');
},
(buffer) => {
const msg = new TextDecoder().decode(buffer);
logger.error(msg, 'fx set');
}
);
token.onCancellationRequested(() => {
cmd?.stop();
});
let exitCode = await cmd?.exitCode;
if (exitCode === 0) {
void window.showInformationMessage('fx set completed');
logger.info('fx set process executed successfully [exit code = 0]', 'fx set');
return Promise.resolve();
}
if (exitCode !== 0) {
logger.warn('fx set stopped', 'fx set');
return Promise.reject(exitCode);
}
});
}
}
);
/**
* Registers fx.build with analytics
*/
registerCommandWithAnalyticsEvent('fuchsia.fx.build', () => {
collection.clear();
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Building Fuchsia',
cancellable: true
}, async (progress, token) => {
let percentage: number; // updated in callback
let cmd = setup.fx.runAsync(['build'], handleData, handleData);
token.onCancellationRequested(() => {
logger.info('fx build stopped: user cancelled operation', 'fx build');
cmd?.stop();
});
let exitCode = await cmd?.exitCode;
if (exitCode === 0) {
logger.info('fx build complete: success [code=0]', 'fx build');
return Promise.resolve();
}
if (exitCode === 1) {
logger.error('fx build stopped: failed [code=1]', 'fx build');
void window.showErrorMessage(
'Build stopped, [see output for details](command:fuchsia.showOutput)'
);
return Promise.reject(exitCode);
}
/**
* Callback to handle buffer to be displayed in vscode UI, sends msg to log
* in output window and updates percent in the progress window.
*/
function handleData(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
let match = msg.match(BUILD_MATCHER);
match ?
logOutput({ message: msg, category: 'fx build', inMatcher: true }) :
logOutput({ message: msg, category: 'fx build', inMatcher: false });
collection.match(msg);
// Surface to user since msg can get lost in output window
if (msg.includes('Locked')) {
void window.showWarningMessage(msg);
}
if (msg.includes('%')) {
const percent = msg.match(/\d+%/);
const lastPercent = percentage ?? 0;
percentage = parseInt(percent![0]);
progress.report({
increment: percentage - lastPercent,
message: `[(details)](command:fuchsia.showOutput) ${match![0]}`
});
}
}
});
});
/** Registers fx.serve with analytics */
registerCommandWithAnalyticsEvent('fuchsia.fx.serve', () => {
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'fx serve',
cancellable: true
}, async (progress, token) => {
let cmd = setup.fx.runAsync(['serve', '--background'], handleData, handleData);
token.onCancellationRequested(() => {
console.log('User canceled fx serve operation');
cmd?.stop();
});
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
/** callback to handle buffer from process */
function handleData(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
logger.info(msg, 'fx serve');
if (msg.includes('resolve')) {
let match = msg.match(/(.*) (\[\w+\])\s+(.*)/);
if (match) {
progress.report({ message: `${match[match.length - 1]}` });
}
}
}
});
});
/** Registers repository.server.stop with analytics */
registerCommandWithAnalyticsEvent('fuchsia.repository.server.stop', () => {
setup.ffx.runFfx(['repository', 'server', 'stop'])
.then(() => {
logger.info('Stopped the repository server', 'fx serve');
})
.catch((err) => {
logger.error(err);
});
});
/** Registers fx.ota with analytics */
registerCommandWithAnalyticsEvent('fuchsia.fx.ota', () => {
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'fx ota',
cancellable: true
}, async (progress, token) => {
let percentage: number;
let cmd = setup.fx.runAsync(['ota', '--build'], handleData, handleData);
token.onCancellationRequested(() => {
logger.info('fx ota stopped: user cancelled operation', 'fx ota');
cmd?.stop();
});
progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
/**
* Callback to handle buffer to be displayed in vscode UI, sends msg to log
* in output window and updates percent in the progress window.
*/
function handleData(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
let match = msg.match(BUILD_MATCHER);
match ?
logOutput({ message: msg, category: 'fx ota', inMatcher: true }) :
logOutput({ message: msg, category: 'fx ota', inMatcher: false });
// Surface to user since msg can get lost in output window
if (msg.includes('Locked')) {
void window.showWarningMessage(msg);
}
if (msg.includes('%')) {
let percent = msg.match(/\d+%/);
let match = msg.match(BUILD_MATCHER);
let lastPercent = percentage ?? 0;
percentage = parseInt(percent![0]);
progress.report({
increment: percentage - lastPercent,
message: `[(details)](command:fuchsia.showOutput) ${match![0]}`
});
}
}
});
});
/** Registers fx.ota.nobuild with analytics */
registerCommandWithAnalyticsEvent('fuchsia.fx.ota.nobuild', () => {
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'fx ota',
cancellable: true
}, async (progress, token) => {
let cmd = setup.fx.runAsync(['ota', '--no-build'], handleData, handleData);
token.onCancellationRequested(() => {
logger.info('fx ota stopped: user cancelled operation', 'fx ota');
cmd?.stop();
});
progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
/**
* Callback to handle buffer to be displayed in vscode UI, sends msg to log
* in output window and updates percent in the progress window.
*/
function handleData(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
let match = msg.match(BUILD_MATCHER);
match ?
logOutput({ message: msg, category: 'fx ota', inMatcher: true }) :
logOutput({ message: msg, category: 'fx ota', inMatcher: false });
// Surface to user since msg can get lost in output window
if (msg.includes('Locked')) {
void window.showWarningMessage(msg);
}
}
});
});
}
/**
* Parameters for the logOutput function
*/
type OutputParams = {
message: string,
category: string,
inMatcher: boolean,
logChannel?: logger.Logger
};
/**
* Determine log level to log output messages for fx build.
* @param output to sort into log level
* @param inMatcher If output is in build targets. There are cases
* where it appears in the file directory and should not output as an error.
* @param logChannel Defaults to the extension logOutputChannel.
*/
export function logOutput(options: OutputParams) {
let msg = options.message.toLowerCase();
let outputChannel = options.logChannel ? options.logChannel : logger;
if (msg.includes('failed: [code=1]')) {
outputChannel.error(options.message, options.category);
return;
}
if (msg.includes('error') || msg.includes('failed')) {
options.inMatcher ?
outputChannel.info(options.message, options.category) :
outputChannel.error(options.message, options.category);
return;
}
if (msg.includes('warn')) {
outputChannel.warn(options.message, options.category);
return;
}
outputChannel.info(options.message, options.category);
}
/**
* Convert output to QuickPickItem[] list
* @param output Array or newline separate string to parse
* @param lastPick Last selection
*/
export function toList(output: string | string[], lastPick?: string,
placeholders?: Map<string, string>): vscode.QuickPickItem[] {
let list: vscode.QuickPickItem[] = [];
let items = Array.isArray(output) ? output : output.split('\n').filter(item => item);
items.forEach((item) => {
if (item === lastPick) {
list.unshift({
label: item,
description: '(Current)',
detail: placeholders?.get(item)
});
return;
}
list.push({
label: item,
detail: placeholders?.get(item)
});
});
return list;
}
/**
* Convert Fuchsia packages to QuickPickItem[] list
* @param output Fuchsia package object to parse
*/
export function packageToList(output: Packages[]): vscode.QuickPickItem[] {
let list: vscode.QuickPickItem[] = [];
output.forEach((item) => {
let packageItems = item.value;
let packageArg = item.notes.substring(0, item.notes.indexOf(' '));
list.push({
label: item.title,
kind: vscode.QuickPickItemKind.Separator,
});
packageItems.forEach((packageItem) => {
list.push({
label: packageItem,
description: packageArg,
picked: true,
});
});
});
return list;
}