blob: 99b9cad372c6a737d829b9b89f9587ed3ccb0cf2 [file]
// 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';
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) {
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 > 1) {
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;
}
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);
runFuchsiaSet(args);
/** Check if 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;
}
/** Get current target configuration */
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;
}
// TODO(https://fxbug.dev/369839772): handle errs w/ task problem matcher
/** handle errs from callback */
function handleErr(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
void window.showErrorMessage(msg);
}
/** 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,
() => void window.showInformationMessage('fx set completed'),
handleErr
);
token.onCancellationRequested(() => {
console.log('User canceled fx set operation');
cmd?.stop();
});
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
});
}
}
);
registerCommandWithAnalyticsEvent('fuchsia.fx.build', () => {
void window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Building Fuchsia',
cancellable: true
}, async (progress, token) => {
let cmd = setup.fx.runAsync(['build'], handleData, handleData);
let percentage: number;
token.onCancellationRequested(() => {
console.log('User canceled fx build operation');
cmd?.stop();
});
/**
* callback to handle buffer from process
*/
function handleData(buffer: Buffer) {
const msg = new TextDecoder().decode(buffer);
let match = msg.match(BUILD_MATCHER);
match ? logOutput(msg, true) : logOutput(msg);
// TODO(https://fxbug.dev/369839772): handle errs w/ task problem matcher
if (msg.includes('build stopped')) {
void window.showErrorMessage(
'Build stopped, [see output for details](command:fuchsia.showOutput)'
);
}
if (msg.includes('Locked')) {
void window.showWarningMessage(msg);
}
if (msg.includes('no work to do')) {
void window.showInformationMessage(msg);
}
if (msg.includes('%')) {
let percent = msg.match(/\d+%/);
let lastPercent = percentage ?? 0;
percentage = parseInt(percent![0]);
progress.report({
increment: percentage - lastPercent,
message: `[(details)](command:fuchsia.showOutput) ${match![0]}`
});
}
}
let exitCode = await cmd?.exitCode;
if (exitCode === 0) {
logger.info('fuchsia build complete', 'fx build');
return Promise.resolve();
}
if (exitCode !== 0) {
return Promise.reject(exitCode);
}
});
});
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]}` });
}
}
}
});
});
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);
});
});
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'],
(buffer: Buffer) => {
let msg = new TextDecoder().decode(buffer);
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]}`
});
}
logger.info(new TextDecoder().decode(buffer), 'fx ota');
},
(buffer: Buffer) => {
logger.error(new TextDecoder().decode(buffer), 'fx ota');
});
token.onCancellationRequested(() => {
console.log('User canceled fx ota');
cmd?.stop();
});
progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
});
});
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'],
(buffer: Buffer) => {
logger.info(new TextDecoder().decode(buffer), 'fx ota');
},
(buffer: Buffer) => {
logger.error(new TextDecoder().decode(buffer), 'fx ota');
});
token.onCancellationRequested(() => {
console.log('User canceled fx ota');
cmd?.stop();
});
progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
let exitCode = await cmd?.exitCode;
return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
});
});
}
/**
* Determine log level to log output messages
* @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(output: string, inMatcher: boolean = false, logChannel?: logger.Logger) {
let msg = output.toLowerCase();
let outputChannel = logChannel ? logChannel : logger;
if (msg.includes('error') || msg.includes('failed')) {
inMatcher ?
outputChannel.info(output, 'fx build') : outputChannel.error(output, 'fx build');
return;
}
if (msg.includes('warn')) {
outputChannel.warn(output, 'fx build');
return;
}
outputChannel.info(output, 'fx build');
}
/**
* 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;
}