1 import { ProgressLocation, Uri, commands, window, workspace } from "vscode";
2 import { Command } from "./command.mjs";
3 import Logger from "../logger.mjs";
4 import { dirname, join } from "path";
5 import { fileURLToPath } from "url";
6 import { type ExecOptions, exec } from "child_process";
7 import { detectInstalledSDKs } from "../utils/picoSDKUtil.mjs";
8 import type Settings from "../settings.mjs";
11 showRquirementsNotMetErrorMessage,
12 } from "../utils/requirementsUtil.mjs";
13 import { SettingsKey } from "../settings.mjs";
14 import { compare } from "../utils/semverUtil.mjs";
16 detectInstalledToolchains,
17 getSupportedToolchains,
18 } from "../utils/toolchainUtil.mjs";
19 import { getSDKReleases } from "../utils/githubREST.mjs";
23 downloadAndInstallSDK,
24 downloadAndInstallToolchain,
25 } from "../utils/download.mjs";
33 consoleOverUART = "Console over UART",
34 consoleOverUSB = "Console over USB (disables other USB use)",
42 interp = "HW interpolation",
44 watch = "HW watchdog",
48 enum PicoWirelessOption {
50 picoWLed = "Pico W onboard LED",
51 picoWPoll = "Polled lwIP",
52 picoWBackground = "Background lwIP",
56 addExamples = "Add examples from Pico library",
57 runFromRAM = "Run the program from RAM rather than flash",
58 cpp = "Generate C++ code",
59 cppRtti = "Enable C++ RTTI (Uses more memory)",
60 cppExceptions = "Enable C++ exceptions (Uses more memory)",
64 debugProbe = "DebugProbe (CMSIS-DAP) [Default]",
65 swd = "SWD (Pi host)",
81 return "-board pico_w";
82 case ConsoleOption.consoleOverUART:
84 case ConsoleOption.consoleOverUSB:
102 case PicoWirelessOption.picoWLed:
103 return "-f picow_led";
104 case PicoWirelessOption.picoWPoll:
105 return "-f picow_poll";
106 case PicoWirelessOption.picoWBackground:
107 return "-f picow_background";
108 case CodeOption.addExamples:
110 case CodeOption.runFromRAM:
114 case CodeOption.cppRtti:
116 case CodeOption.cppExceptions:
118 case Debugger.debugProbe:
123 // TODO: maybe just return an empty string
124 throw new Error(`Unknown enum value: ${e as string}`);
128 interface NewProjectOptions {
131 boardType: BoardType;
132 consoleOptions: ConsoleOption[];
133 libraries: Array<Library | PicoWirelessOption>;
134 codeOptions: CodeOption[];
137 toolchainVersion: string;
138 toolchainPath: string;
144 function getScriptsRoot(): string {
145 return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
148 export default class NewProjectCommand extends Command {
149 private readonly _logger: Logger = new Logger("NewProjectCommand");
150 private readonly _settings: Settings;
152 constructor(settings: Settings) {
155 this._settings = settings;
158 private async selectSDKAndToolchain(): Promise<
160 toolchainVersion: string;
161 toolchainPath: string;
167 const installedSDKs = detectInstalledSDKs();
168 const installedToolchains = detectInstalledToolchains();
171 if (installedSDKs.length === 0 || installedToolchains.length === 0) {
172 // TODO: add offline handling
173 const availableSDKs = await getSDKReleases();
174 const supportedToolchains = await getSupportedToolchains();
176 // show quick pick for sdk and toolchain
177 const selectedSDK = await window.showQuickPick(
178 availableSDKs.map(sdk => ({
179 label: `v${sdk.tagName}`,
183 placeHolder: "Select Pico SDK version",
184 title: "New Pico Project",
188 if (selectedSDK === undefined) {
192 // show quick pick for toolchain version
193 const selectedToolchain = await window.showQuickPick(
194 supportedToolchains.map(toolchain => ({
195 label: toolchain.version.replaceAll("_", "."),
196 toolchain: toolchain,
199 placeHolder: "Select ARM Embeded Toolchain version",
200 title: "New Pico Project",
204 if (selectedToolchain === undefined) {
208 // show user feedback as downloads can take a while
209 let installedSuccessfully = false;
210 await window.withProgress(
212 location: ProgressLocation.Notification,
213 title: "Downloading SDK and Toolchain",
219 !(await downloadAndInstallSDK(
220 selectedSDK.sdk.tagName,
221 selectedSDK.sdk.downloadUrl
223 !(await downloadAndInstallToolchain(selectedToolchain.toolchain))
225 Logger.log(`Failed to download and install toolchain and SDK.`);
228 message: "Failed to download and install toolchain and sdk.",
232 installedSuccessfully = false;
234 installedSuccessfully = true;
239 if (!installedSuccessfully) {
243 toolchainVersion: selectedToolchain.toolchain.version,
244 toolchainPath: buildToolchainPath(
245 selectedToolchain.toolchain.version
247 sdkVersion: selectedSDK.sdk.tagName,
248 sdkPath: buildSDKPath(selectedSDK.sdk.tagName),
253 // sort installed sdks from newest to oldest
254 installedSDKs.sort((a, b) =>
255 compare(a.version.replace("v", ""), b.version.replace("v", ""))
257 // toolchains should be sorted and cant be sorted by compare because
258 // of their alphanumeric structure
261 toolchainVersion: installedToolchains[0].version,
262 toolchainPath: installedToolchains[0].path,
263 sdkVersion: installedSDKs[0].version,
264 sdkPath: installedSDKs[0].sdkPath,
268 `Error while retrieving SDK and toolchain versions: ${
269 error instanceof Error ? error.message : (error as string)
273 void window.showErrorMessage(
274 "Error while retrieving SDK and toolchain versions."
281 async execute(): Promise<void> {
282 // check if all requirements are met
283 if (!(await checkForRequirements(this._settings))) {
284 void showRquirementsNotMetErrorMessage();
289 // TODO: maybe make it posible to also select a folder and
290 // not always create a new one with selectedName
291 const projectRoot: Uri[] | undefined = await window.showOpenDialog({
292 canSelectFiles: false,
293 canSelectFolders: true,
294 canSelectMany: false,
295 openLabel: "Select project root",
298 // user focused out of the quick pick
299 if (!projectRoot || projectRoot.length !== 1) {
304 const selectedName: string | undefined = await window.showInputBox({
305 placeHolder: "Enter a project name",
306 title: "New Pico Project",
309 // user focused out of the quick pick
314 // get board type (single selection)
315 const selectedBoardType: BoardType | undefined =
316 (await window.showQuickPick(Object.values(BoardType), {
317 placeHolder: "Select a board type",
318 title: "New Pico Project",
319 })) as BoardType | undefined;
321 // user focused out of the quick pick
322 if (!selectedBoardType) {
326 // [optional] get console options (multi selection)
327 const selectedConsoleOptions: ConsoleOption[] | undefined =
328 (await window.showQuickPick(Object.values(ConsoleOption), {
329 placeHolder: "Would you like to enable the USB console?",
330 title: "New Pico Project",
332 })) as ConsoleOption[] | undefined;
334 // user focused out of the quick pick
335 if (!selectedConsoleOptions) {
339 // [optional] get libraries (multi selection)
340 const selectedFeatures: Array<Library | PicoWirelessOption> | undefined =
341 (await window.showQuickPick(Object.values(Library), {
342 placeHolder: "Select libraries to include",
343 title: "New Pico Project",
345 })) as Library[] | undefined;
347 // user focused out of the quick pick
348 if (!selectedFeatures) {
352 if (selectedBoardType === BoardType.picoW) {
353 const selectedWirelessFeature: PicoWirelessOption | undefined =
354 (await window.showQuickPick(Object.values(PicoWirelessOption), {
356 "Select wireless features to include or press enter to skip",
357 title: "New Pico Project",
359 })) as PicoWirelessOption | undefined;
361 // user focused out of the quick pick
362 if (!selectedWirelessFeature) {
367 selectedWirelessFeature &&
368 selectedWirelessFeature !== PicoWirelessOption.none
370 selectedFeatures.push(selectedWirelessFeature);
374 const selectedCodeOptions: CodeOption[] | undefined =
375 (await window.showQuickPick(Object.values(CodeOption), {
376 placeHolder: "Select code generator options to use",
377 title: "New Pico Project",
379 })) as CodeOption[] | undefined;
381 if (!selectedCodeOptions) {
385 const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
386 Object.values(Debugger),
388 placeHolder: "Select debugger to use",
389 title: "New Pico Project",
392 )) as Debugger | undefined;
394 if (!selectedDebugger) {
398 const selectedToolchainAndSDK = await this.selectSDKAndToolchain();
400 if (selectedToolchainAndSDK === undefined) {
404 void window.showWarningMessage(
405 "Generating project, this may take a while. " +
406 "For linting and auto-complete to work, " +
407 "please completly restart VSCode after the project has been generated."
410 await this.executePicoProjectGenerator({
412 projectRoot: projectRoot[0].fsPath,
413 boardType: selectedBoardType,
414 consoleOptions: selectedConsoleOptions,
415 libraries: selectedFeatures,
416 codeOptions: selectedCodeOptions,
417 debugger: selectedDebugger,
418 toolchainAndSDK: selectedToolchainAndSDK,
422 private runGenerator(
425 ): Promise<number | null> {
426 return new Promise<number | null>(resolve => {
427 const generatorProcess = exec(command, options, error => {
429 console.error(`Error: ${error.message}`);
430 resolve(null); // Indicate error
434 generatorProcess.on("exit", code => {
435 // Resolve with exit code or -1 if code is undefined
442 * Executes the Pico Project Generator with the given options.
444 * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
446 private async executePicoProjectGenerator(
447 options: NewProjectOptions
449 const customEnv: { [key: string]: string } = {
450 ...(process.env as { [key: string]: string }),
452 ["PICO_SDK_PATH"]: options.toolchainAndSDK.sdkPath,
453 // set PICO_TOOLCHAIN_PATH i needed someday
454 ["PICO_TOOLCHAIN_PATH"]: options.toolchainAndSDK.toolchainPath,
456 // add compiler to PATH
457 const isWindows = process.platform === "win32";
458 customEnv[isWindows ? "Path" : "PATH"] = `${join(
459 options.toolchainAndSDK.toolchainPath,
461 )}${isWindows ? ";" : ":"}${customEnv[isWindows ? "Path" : "PATH"]}`;
463 this._settings.getString(SettingsKey.python3Path) || isWindows
467 const command: string = [
469 join(getScriptsRoot(), "pico_project.py"),
470 enumToParam(options.boardType),
471 ...options.consoleOptions.map(option => enumToParam(option)),
472 !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
475 ...options.libraries.map(option => enumToParam(option)),
476 ...options.codeOptions.map(option => enumToParam(option)),
477 enumToParam(options.debugger),
478 // generate .vscode config
482 `"${options.projectRoot}"`,
484 options.toolchainAndSDK.sdkVersion,
485 "--toolchainVersion",
486 options.toolchainAndSDK.toolchainVersion,
490 this._logger.debug(`Executing project generator command: ${command}`);
493 // TODO: use exit codes to determine why the project generator failed (if it did)
494 // to be able to show the user a more detailed error message
495 const generatorExitCode = await this.runGenerator(command, {
497 cwd: getScriptsRoot(),
501 if (generatorExitCode === 0) {
502 void window.showInformationMessage(
503 `Successfully generated new project: ${options.name}`
507 void commands.executeCommand(
509 Uri.file(join(options.projectRoot, options.name)),
510 (workspace.workspaceFolders?.length ?? 0) > 0
514 `Generator Process exited with code: ${generatorExitCode ?? "null"}`
517 void window.showErrorMessage(
518 `Could not create new project: ${options.name}`