1 import { exec } from "child_process";
2 import { workspace, type Uri, window, ProgressLocation } from "vscode";
3 import { showRequirementsNotMetErrorMessage } from "./requirementsUtil.mjs";
4 import { dirname, join, resolve } from "path";
5 import Settings from "../settings.mjs";
6 import { HOME_VAR, SettingsKey } from "../settings.mjs";
7 import { existsSync, readFileSync, rmSync } from "fs";
8 import Logger from "../logger.mjs";
9 import { readFile, writeFile } from "fs/promises";
10 import { rimraf, windows as rimrafWindows } from "rimraf";
11 import { homedir } from "os";
12 import which from "which";
13 import { compareLtMajor } from "./semverUtil.mjs";
14 import { buildCMakeIncPath } from "./download.mjs";
16 export const CMAKE_DO_NOT_EDIT_HEADER_PREFIX =
17 // eslint-disable-next-line max-len
18 "== DO NEVER EDIT THE NEXT LINES for Raspberry Pi Pico VS Code Extension to work ==";
20 export async function getPythonPath(): Promise<string> {
21 const settings = Settings.getInstance();
22 if (settings === undefined) {
23 Logger.log("Error: Settings not initialized.");
31 .getString(SettingsKey.python3Path)
32 ?.replace(HOME_VAR, homedir()) ||
33 (process.platform === "win32" ? "python" : "python3"),
36 ).replaceAll("\\", "/");
38 return `${pythonPath.replaceAll("\\", "/")}`;
41 export async function getPath(): Promise<string> {
42 const settings = Settings.getInstance();
43 if (settings === undefined) {
44 Logger.log("Error: Settings not initialized.");
51 settings.getString(SettingsKey.ninjaPath)?.replace(HOME_VAR, homedir()) ||
55 ).replaceAll("\\", "/");
58 settings.getString(SettingsKey.cmakePath)?.replace(HOME_VAR, homedir()) ||
62 ).replaceAll("\\", "/");
64 settings.getString(SettingsKey.python3Path)?.replace(HOME_VAR, homedir())
66 // TODO: maybe also check for "python" on unix systems
67 const pythonPath = await getPythonPath();
69 if (ninjaPath.length === 0 || cmakePath.length === 0) {
70 const missingTools = [];
71 if (ninjaPath.length === 0) {
72 missingTools.push("Ninja");
74 if (cmakePath.length === 0) {
75 missingTools.push("CMake");
77 if (pythonPath.length === 0) {
78 missingTools.push("Python 3");
80 void showRequirementsNotMetErrorMessage(missingTools);
85 const isWindows = process.platform === "win32";
87 return `${ninjaPath.includes("/") ? dirname(ninjaPath) : ""}${
88 cmakePath.includes("/")
89 ? `${isWindows ? ";" : ":"}${dirname(cmakePath)}`
92 pythonPath.includes("/")
93 ? `${dirname(pythonPath)}${isWindows ? ";" : ":"}`
98 export async function configureCmakeNinja(folder: Uri): Promise<boolean> {
99 if (process.platform !== "win32" && folder.fsPath.includes("\\")) {
101 "CMake currently does not support folder names with backslashes.";
102 Logger.log(errorMsg);
103 await window.showErrorMessage(
104 "Failed to configure cmake for the current project. " + errorMsg
110 const settings = Settings.getInstance();
111 if (settings === undefined) {
112 Logger.log("Error: Settings not initialized.");
117 if (settings.getBoolean(SettingsKey.useCmakeTools)) {
118 await window.showErrorMessage(
119 "You must use the CMake Tools extension to configure your build. " +
120 "To use this extension instead, change the useCmakeTools setting."
126 if (existsSync(join(folder.fsPath, "build", "CMakeCache.txt"))) {
127 // check if the build directory has been moved
129 const buildDir = join(folder.fsPath, "build");
131 const cacheBuildDir = cmakeGetPicoVar(
132 join(buildDir, "CMakeCache.txt"),
133 "CMAKE_CACHEFILE_DIR"
136 if (cacheBuildDir !== null) {
137 let p1 = resolve(buildDir);
138 let p2 = resolve(cacheBuildDir);
139 if (process.platform === "win32") {
140 p1 = p1.toLowerCase();
141 p2 = p2.toLowerCase();
146 `Build directory has been moved from ${p1} to ${p2}` +
147 ` - Deleting CMakeCache.txt and regenerating.`
150 rmSync(join(buildDir, "CMakeCache.txt"));
156 // check if CMakeLists.txt exists in the root folder
157 await workspace.fs.stat(
158 folder.with({ path: join(folder.fsPath, "CMakeLists.txt") })
161 void window.withProgress(
163 location: ProgressLocation.Notification,
165 title: "Configuring CMake...",
167 async (progress, token) => {
170 .getString(SettingsKey.cmakePath)
171 ?.replace(HOME_VAR, homedir().replaceAll("\\", "/")) || "cmake";
173 // TODO: analyze command result
174 // TODO: option for the user to choose the generator
175 // TODO: maybe delete the build folder before running cmake so
176 // all configuration files in build get updates
177 const customEnv = process.env;
178 /*customEnv["PYTHONHOME"] = pythonPath.includes("/")
179 ? resolve(join(dirname(pythonPath), ".."))
181 const isWindows = process.platform === "win32";
182 const customPath = await getPath();
186 customEnv[isWindows ? "Path" : "PATH"] =
187 customPath + customEnv[isWindows ? "Path" : "PATH"];
188 const pythonPath = await getPythonPath();
192 process.env.ComSpec === "powershell.exe" ? "&" : ""
194 pythonPath.includes("/")
195 ? `-DPython3_EXECUTABLE="${pythonPath.replaceAll("\\", "/")}" `
197 }` + `-G Ninja -B ./build "${folder.fsPath}"`;
205 (error, stdout, stderr) => {
207 console.error(error);
208 console.log(`stdout: ${stdout}`);
209 console.log(`stderr: ${stderr}`);
216 child.on("error", err => {
220 //child.stdout?.on("data", data => {});
221 child.on("close", () => {
222 progress.report({ increment: 100 });
224 child.on("exit", code => {
226 console.error(`CMake exited with code ${code ?? "unknown"}`);
228 progress.report({ increment: 100 });
231 token.onCancellationRequested(() => {
244 * Changes the board in the CMakeLists.txt file.
246 * @param folder The root folder of the workspace to configure.
247 * @param newBoard The new board to use
249 export async function cmakeUpdateBoard(
252 ): Promise<boolean> {
253 // TODO: support for scaning for seperate locations of the CMakeLists.txt file in the project
254 const cmakeFilePath = join(folder.fsPath, "CMakeLists.txt");
255 const picoBoardRegex = /^set\(PICO_BOARD\s+([^)]+)\)$/m;
257 const settings = Settings.getInstance();
258 if (settings === undefined) {
259 Logger.log("Error: Settings not initialized.");
265 // check if CMakeLists.txt exists in the root folder
266 await workspace.fs.stat(folder.with({ path: cmakeFilePath }));
268 const content = await readFile(cmakeFilePath, "utf8");
270 const modifiedContent = content.replace(
272 `set(PICO_BOARD ${newBoard} CACHE STRING "Board type")`
275 await writeFile(cmakeFilePath, modifiedContent, "utf8");
276 Logger.log("Updated board in CMakeLists.txt successfully.");
278 // reconfigure so .build gets updated
279 // TODO: To get a behavior similar to the rm -rf Unix command,
280 // use rmSync with options { recursive: true, force: true }
281 // to remove rimraf requirement
282 if (process.platform === "win32") {
283 await rimrafWindows(join(folder.fsPath, "build"), { maxRetries: 2 });
285 await rimraf(join(folder.fsPath, "build"), { maxRetries: 2 });
287 await configureCmakeNinja(folder);
288 Logger.log("Reconfigured CMake successfully.");
292 Logger.log("Error updating board in CMakeLists.txt!");
299 * Updates the sdk and toolchain relay paths in the CMakeLists.txt file.
301 * @param folder The root folder of the workspace to configure.
302 * @param newSDKVersion The verison in "$HOME/.picosdk/sdk/${newSDKVersion}"
303 * @param newToolchainVersion The verison in "$HOME/.picosdk/toolchain/${newToolchainVersion}"
305 export async function cmakeUpdateSDK(
307 newSDKVersion: string,
308 newToolchainVersion: string,
309 newPicotoolVersion: string,
310 reconfigure: boolean = true
311 ): Promise<boolean> {
312 // TODO: support for scaning for seperate locations of the CMakeLists.txt file in the project
313 const cmakeFilePath = join(folder.fsPath, "CMakeLists.txt");
314 // This regex requires multiline (m) and dotall (s) flags to work
315 const updateSectionRegex =
316 new RegExp(`^# ${CMAKE_DO_NOT_EDIT_HEADER_PREFIX}.*# =+$`, "ms");
317 const picoBoardRegex = /^set\(PICO_BOARD\s+([^)]+)\)$/m;
319 const settings = Settings.getInstance();
320 if (settings === undefined) {
321 Logger.log("Error: Settings not initialized.");
327 // check if CMakeLists.txt exists in the root folder
328 await workspace.fs.stat(folder.with({ path: cmakeFilePath }));
330 const content = await readFile(cmakeFilePath, "utf8");
332 let modifiedContent = content
335 `# ${CMAKE_DO_NOT_EDIT_HEADER_PREFIX}\n` +
337 " set(USERHOME $ENV{USERPROFILE})\n" +
339 " set(USERHOME $ENV{HOME})\n" +
341 `set(sdkVersion ${newSDKVersion})\n` +
342 `set(toolchainVersion ${newToolchainVersion})\n` +
343 `set(picotoolVersion ${newPicotoolVersion})\n` +
344 `set(picoVscode ${buildCMakeIncPath(false)}/pico-vscode.cmake)\n` +
345 "if (EXISTS ${picoVscode})\n" +
346 " include(${picoVscode})\n" +
348 // eslint-disable-next-line max-len
349 "# ===================================================================================="
352 const picoBoard = content.match(picoBoardRegex);
353 // update the PICO_BOARD variable if it's a pico2 board and the new sdk
354 // version is less than 2.0.0
356 picoBoard !== null &&
357 picoBoard[1].includes("pico2") &&
358 compareLtMajor(newSDKVersion, "2.0.0")
360 const result = await window.showQuickPick(["pico", "pico_w"], {
361 placeHolder: "The new SDK version does not support your current board",
363 ignoreFocusOut: true,
364 title: "Please select a new board type",
367 if (result === undefined) {
368 Logger.log("User canceled board type selection during SDK update.");
373 modifiedContent = modifiedContent.replace(
375 `set(PICO_BOARD ${result} CACHE STRING "Board type")`
379 await writeFile(cmakeFilePath, modifiedContent, "utf8");
380 Logger.log("Updated paths in CMakeLists.txt successfully.");
383 // reconfigure so .build gets updated
384 // TODO: To get a behavior similar to the rm -rf Unix command,
385 // use rmSync with options { recursive: true, force: true }
386 // to remove rimraf requirement
387 if (process.platform === "win32") {
388 await rimrafWindows(join(folder.fsPath, "build"), { maxRetries: 2 });
390 await rimraf(join(folder.fsPath, "build"), { maxRetries: 2 });
392 await configureCmakeNinja(folder);
393 Logger.log("Reconfigured CMake successfully.");
398 Logger.log("Error updating paths in CMakeLists.txt!");
405 * Extracts the sdk and toolchain versions from the CMakeLists.txt file.
407 * @param cmakeFilePath The path to the CMakeLists.txt file.
408 * @returns An tupple with the [sdk, toolchain, picotool] versions or null if the file could not
409 * be read or the versions could not be extracted.
411 export async function cmakeGetSelectedToolchainAndSDKVersions(
413 ): Promise<[string, string, string] | null> {
414 const cmakeFilePath = join(folder.fsPath, "CMakeLists.txt");
415 const content = readFileSync(cmakeFilePath, "utf8");
418 const sdkPathRegex = /^set\(PICO_SDK_PATH\s+([^)]+)\)$/m;
419 const toolchainPathRegex = /^set\(PICO_TOOLCHAIN_PATH\s+([^)]+)\)$/m;
420 const oldMatch = content.match(sdkPathRegex);
421 const oldMatch2 = content.match(toolchainPathRegex);
424 const sdkVersionRegex = /^set\(sdkVersion\s+([^)]+)\)$/m;
425 const toolchainVersionRegex = /^set\(toolchainVersion\s+([^)]+)\)$/m;
426 const picotoolVersionRegex = /^set\(picotoolVersion\s+([^)]+)\)$/m;
427 const match = content.match(sdkVersionRegex);
428 const match2 = content.match(toolchainVersionRegex);
429 const match3 = content.match(picotoolVersionRegex);
431 if (match !== null && match2 !== null && match3 !== null) {
432 return [match[1], match2[1], match3[1]];
433 } else if (oldMatch !== null && oldMatch2 !== null) {
434 const path = oldMatch[1];
435 const path2 = oldMatch2[1];
436 const versionRegex = /^\${USERHOME}\/\.pico-sdk\/sdk\/([^)]+)$/m;
437 const versionRegex2 = /^\${USERHOME}\/\.pico-sdk\/toolchain\/([^)]+)$/m;
438 const versionMatch = path.match(versionRegex);
439 const versionMatch2 = path2.match(versionRegex2);
441 if (versionMatch === null || versionMatch2 === null) {
445 Logger.log("Updating extension lines in CMake file");
446 await cmakeUpdateSDK(
447 folder, versionMatch[1], versionMatch2[1], versionMatch[1]
449 Logger.log("Extension lines updated");
451 return [versionMatch[1], versionMatch2[1], versionMatch[1]];
458 * Extracts the board from the CMakeLists.txt file.
460 * @param cmakeFilePath The path to the CMakeLists.txt file.
461 * @returns An string with the board or null if the file could not
462 * be read or the board could not be extracted.
464 export function cmakeGetSelectedBoard(folder: Uri): string | null {
465 const cmakeFilePath = join(folder.fsPath, "CMakeLists.txt");
466 const content = readFileSync(cmakeFilePath, "utf8");
468 const picoBoardRegex = /^set\(PICO_BOARD\s+([^)]+)\)$/m;
470 const match = content.match(picoBoardRegex);
472 if (match !== null) {
473 const board = match[1].split("CACHE")[0].trim();
475 if (board === null) {
486 * Extracts the picoVar from the CMakeCache.txt file.
488 * @param cmakeFilePath The path to the CMakeCache.txt file.
489 * @returns The variable or null if the file could not
490 * be read or the variable could not be extracted.
492 export function cmakeGetPicoVar(
493 cmakeFilePath: string,
496 const content = readFileSync(cmakeFilePath, "utf8");
497 const picoVarRegex = new RegExp(`^${picoVar}:.*=(.*)$`, "m");
498 const match = content.match(picoVarRegex);
500 if (match === null) {