From e54534b871a96f5b477bca0cd2039b414ed1ee27 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:49:06 +0100 Subject: [PATCH 1/2] Support for auto-configure cmake when snippets are selected Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/utils/cmakeUtil.mts | 12 ++++++++- src/utils/setupZephyr.mts | 52 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/utils/cmakeUtil.mts b/src/utils/cmakeUtil.mts index 90ce3c3..838c4a3 100644 --- a/src/utils/cmakeUtil.mts +++ b/src/utils/cmakeUtil.mts @@ -18,6 +18,7 @@ import State from "../state.mjs"; import { generateCustomZephyrEnv, getBoardFromZephyrProject, + zephyrGetSelectedSnippets, } from "./setupZephyr.mjs"; export const CMAKE_DO_NOT_EDIT_HEADER_PREFIX = @@ -248,12 +249,21 @@ export async function configureCmakeNinja( return false; } - const zephyrCommand = `${ + let zephyrCommand = `${ process.env.ComSpec?.endsWith("powershell.exe") ? "&" : "" }"${westPath}" build --cmake-only -b ${ zephyrBoard ?? "" } -d "${buildDir}" "${folder.fsPath}"`; + // check for selected snippets and include for cmake configuration + if (isZephyrProject) { + const snippets = await zephyrGetSelectedSnippets(folder); + + for (const snippet of snippets) { + zephyrCommand += ` -S ${snippet}`; + } + } + await new Promise((resolve, reject) => { // use exec to be able to cancel the process const child = exec( diff --git a/src/utils/setupZephyr.mts b/src/utils/setupZephyr.mts index fbd4e97..63edc45 100644 --- a/src/utils/setupZephyr.mts +++ b/src/utils/setupZephyr.mts @@ -574,6 +574,9 @@ async function showNoWgetError(): Promise { "wget not found in Path. Please install wget and ensure " + "it is available in Path. " + "See the Zephyr notes in the pico-vscode README for guidance.", + { + modal: true, + }, "Open README" ); if (response === "Open README") { @@ -593,7 +596,7 @@ async function checkMacosLinuxDeps(): Promise { const wget = await which("wget", { nothrow: true }); if (!wget) { - await showNoWgetError(); + void showNoWgetError(); return false; } @@ -628,7 +631,7 @@ async function checkWindowsDeps(): Promise { const wget = await which("wget", { nothrow: true }); if (!wget) { - await showNoWgetError(); + void showNoWgetError(); return false; } @@ -1718,3 +1721,48 @@ export async function zephyrVerifyCMakeCache( return; } } + +export async function zephyrGetSelectedSnippets( + workspaceUri: Uri +): Promise { + const snippetsUri = Uri.joinPath(workspaceUri, ".vscode", "tasks.json"); + + // search for "Compile Project" get every i+1 where i is an index and args[i]=="-S" || "--snippet" + try { + await workspace.fs.stat(snippetsUri); + + const td = new TextDecoder("utf-8"); + const tasksJson = JSON.parse( + td.decode(await workspace.fs.readFile(snippetsUri)) + ) as { tasks: ITask[] }; + + const compileTask = tasksJson.tasks.find( + t => t.label === "Compile Project" + ); + if (compileTask === undefined) { + return []; + } + + const selectedSnippets: string[] = []; + for (let i = 0; i < compileTask.args.length; i++) { + if (compileTask.args[i] === "-S" || compileTask.args[i] === "--snippet") { + if (i + 1 < compileTask.args.length) { + selectedSnippets.push(compileTask.args[i + 1]); + } + } + } + + return selectedSnippets; + } catch (error) { + Logger.warn( + LoggerSource.zephyrSetup, + `Failed to read tasks.json file: ${unknownErrorToString(error)}` + ); + void window.showWarningMessage( + "Failed to read tasks.json file. " + + "Make sure the file exists and has a Compile Project task." + ); + + return []; + } +} From da6ce4c32f34e699ba22e102ee8c884cfa29c3fa Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:40:27 +0100 Subject: [PATCH 2/2] Support for zephyr-sdk 1.0.0 + added uninstaller to quick access Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- package.json | 4 +- src/utils/semverUtil.mts | 84 ++++++++++++++++++++++++++++++++ src/utils/setupZephyr.mts | 73 +++++++++++++++++++++++++++ src/webview/activityBar.mts | 13 +++++ src/webview/uninstallerPanel.mts | 4 +- 5 files changed, 174 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 49f9ef5..f9c51fb 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "command": "raspberry-pi-pico.switchSDK", "title": "Switch Pico SDK", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject && !raspberry-pi-pico.isZephyrProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.switchBoard", @@ -277,7 +277,7 @@ }, { "command": "raspberry-pi-pico.openUninstaller", - "title": "Open Uninstaller", + "title": "Manage Installed Components", "category": "Raspberry Pi Pico" }, { diff --git a/src/utils/semverUtil.mts b/src/utils/semverUtil.mts index 382714c..fa1cc79 100644 --- a/src/utils/semverUtil.mts +++ b/src/utils/semverUtil.mts @@ -141,3 +141,87 @@ export function compareLtMajor(first: string, second: string): boolean { // Compare only the major versions return firstMajor < secondMajor; } + +export type Pre = { tag: "alpha" | "beta" | "rc" | null; num: number }; + +export type ParsedVer = { + major: number; + minor: number; + patch: number; + pre: Pre; // null tag means "final" +}; + +function parseVer(input: string): ParsedVer | null { + const s = input.trim().toLowerCase().replace(/^v/, ""); + const [core, preRaw = ""] = s.split("-", 2); + + // allow 2 or 3 core parts; pad missing with 0 + const parts = core.split(".").map(n => Number(n)); + if (parts.length < 2 || parts.length > 3) { + return null; + } + const [major, minor, patch = 0] = parts; + if (![major, minor, patch].every(n => Number.isInteger(n) && n >= 0)) { + return null; + } + + let pre: Pre = { tag: null, num: 0 }; + if (preRaw) { + // accept alpha/beta/rc with optional number (default 0) + const m = /^(alpha|beta|rc)(\d+)?$/.exec(preRaw); + if (!m) { + return null; + } + pre = { tag: m[1] as Pre["tag"], num: m[2] ? Number(m[2]) : 0 }; + } + + return { major, minor, patch, pre }; +} + +const rank: Record | "final", number> = { + alpha: 0, + beta: 1, + rc: 2, + final: 3, +}; + +/** Compare a vs b: -1 if ab */ +export function compareSemverPre(a: string, b: string): number { + const A = parseVer(a), + B = parseVer(b); + if (!A || !B) { + throw new Error(`Invalid version: "${a}" or "${b}"`); + } + + if (A.major !== B.major) { + return A.major < B.major ? -1 : 1; + } + if (A.minor !== B.minor) { + return A.minor < B.minor ? -1 : 1; + } + if (A.patch !== B.patch) { + return A.patch < B.patch ? -1 : 1; + } + + // prerelease ranking: alpha < beta < rc < final + const rA = rank[A.pre.tag ?? "final"]; + const rB = rank[B.pre.tag ?? "final"]; + if (rA !== rB) { + return rA < rB ? -1 : 1; + } + + // same prerelease tag (or both final) + if (A.pre.tag === null) { + return 0; + } // both final + if (A.pre.num !== B.pre.num) { + return A.pre.num < B.pre.num ? -1 : 1; + } + + return 0; +} + +export const geSemverPre = (a: string, b: string): boolean => + compareSemverPre(a, b) >= 0; +export const ltSemverPre = (a: string, b: string): boolean => + compareSemverPre(a, b) < 0; diff --git a/src/utils/setupZephyr.mts b/src/utils/setupZephyr.mts index 63edc45..c38109e 100644 --- a/src/utils/setupZephyr.mts +++ b/src/utils/setupZephyr.mts @@ -41,6 +41,7 @@ import type { ITask } from "../models/task.mjs"; import { getWestConfigValue, updateZephyrBase } from "./westConfig.mjs"; import { addZephyrVariant } from "./westManifest.mjs"; import LastUsedDepsStore from "./lastUsedDeps.mjs"; +import { geSemverPre } from "./semverUtil.mjs"; interface ZephyrSetupValue { cmakeMode: number; @@ -1631,6 +1632,78 @@ export async function updateZephyrCompilerPath( } } + // support for v1.0.0 zephyr sdk toolchain location change + const launchUri = Uri.joinPath(workspaceUri, ".vscode", "launch.json"); + if (geSemverPre(sdkVersion, "v1.0.0-beta1")) { + zephyrConfig.compilerPath.replace( + `${sdkVersion}/arm-zephyr-eabi`, + `${sdkVersion}/gnu/arm-zephyr-eabi` + ); + + try { + await workspace.fs.stat(launchUri); + + const launchJson = JSON.parse( + td.decode(await workspace.fs.readFile(launchUri)) + ) as { + configurations: Array<{ name: string; armToolchainPath: string }>; + }; + + const picoDebugConfig = launchJson.configurations.find( + c => c.name === "Pico Debug (Zephyr)" + ); + if (picoDebugConfig) { + picoDebugConfig.armToolchainPath = + picoDebugConfig.armToolchainPath.replace( + "}/arm-zephyr-eabi/", + "}/gnu/arm-zephyr-eabi/" + ); + + const te = new TextEncoder(); + await workspace.fs.writeFile( + launchUri, + te.encode(JSON.stringify(launchJson, null, 2)) + ); + } + } catch { + // do nothing + } + } else { + zephyrConfig.compilerPath.replace( + `${sdkVersion}/gnu/arm-zephyr-eabi`, + `${sdkVersion}/arm-zephyr-eabi` + ); + + try { + await workspace.fs.stat(launchUri); + + const launchJson = JSON.parse( + td.decode(await workspace.fs.readFile(launchUri)) + ) as { + configurations: Array<{ name: string; armToolchainPath: string }>; + }; + + const picoDebugConfig = launchJson.configurations.find( + c => c.name === "Pico Debug (Zephyr)" + ); + if (picoDebugConfig) { + picoDebugConfig.armToolchainPath = + picoDebugConfig.armToolchainPath.replace( + "}/gnu/arm-zephyr-eabi/", + "}/arm-zephyr-eabi/" + ); + + const te = new TextEncoder(); + await workspace.fs.writeFile( + launchUri, + te.encode(JSON.stringify(launchJson, null, 2)) + ); + } + } catch { + // do nothing + } + } + const te = new TextEncoder(); await workspace.fs.writeFile( cppPropertiesUri, diff --git a/src/webview/activityBar.mts b/src/webview/activityBar.mts index 6a520a6..53570eb 100644 --- a/src/webview/activityBar.mts +++ b/src/webview/activityBar.mts @@ -24,6 +24,7 @@ import { NEW_EXAMPLE_PROJECT, NEW_PROJECT, OPEN_SDK_DOCUMENTATION, + OPEN_UNINSTALLER, RUN_PROJECT, SWITCH_BOARD, SWITCH_BUILD_TYPE, @@ -61,6 +62,7 @@ const CLEAN_CMAKE_PROJECT_LABEL = "Clean CMake"; const SWITCH_BUILD_TYPE_LABEL = "Switch Build Type"; const DEBUG_PROJECT_LABEL = "Debug Project"; const DEBUG_LAYOUT_PROJECT_LABEL = "Debug Layout"; +const MANAGE_COMPONENTS_LABEL = "Manage Components"; export class PicoProjectActivityBar implements TreeDataProvider @@ -126,6 +128,9 @@ export class PicoProjectActivityBar // alt. "file-code" element.iconPath = new ThemeIcon("file-symlink-directory"); break; + case MANAGE_COMPONENTS_LABEL: + element.iconPath = new ThemeIcon("package"); + break; case DEBUG_PROJECT_LABEL: element.iconPath = new ThemeIcon("debug-alt"); @@ -253,6 +258,14 @@ export class PicoProjectActivityBar arguments: [true], } ), + new QuickAccessCommand( + MANAGE_COMPONENTS_LABEL, + TreeItemCollapsibleState.None, + { + command: `${extensionName}.${OPEN_UNINSTALLER}`, + title: MANAGE_COMPONENTS_LABEL, + } + ), ]; } else if (element.label === PROJECT_COMMANDS_PARENT_LABEL) { return [ diff --git a/src/webview/uninstallerPanel.mts b/src/webview/uninstallerPanel.mts index 1cdaad5..59e718b 100644 --- a/src/webview/uninstallerPanel.mts +++ b/src/webview/uninstallerPanel.mts @@ -45,7 +45,7 @@ export class UninstallerPanel { const panel = window.createWebviewPanel( UninstallerPanel.viewType, - "Pico SDK Uninstaller", + "Manage Installed Components", column || ViewColumn.One, getWebviewOptions(extensionUri) ); @@ -176,7 +176,7 @@ export class UninstallerPanel { } private async _update(): Promise { - this._panel.title = "Pico SDK Uninstaller"; + this._panel.title = "Manage Installed Components"; this._panel.iconPath = Uri.joinPath( this._extensionUri,