]> Git Repo - pico-vscode.git/blob - src/commands/newProject.mts
Removed reload VS Code warning
[pico-vscode.git] / src / commands / newProject.mts
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";
9 import {
10   checkForRequirements,
11   showRequirementsNotMetErrorMessage,
12 } from "../utils/requirementsUtil.mjs";
13 import { SettingsKey } from "../settings.mjs";
14 import { compare } from "../utils/semverUtil.mjs";
15 import {
16   detectInstalledToolchains,
17   getSupportedToolchains,
18 } from "../utils/toolchainUtil.mjs";
19 import { SDK_REPOSITORY_URL, getSDKReleases } from "../utils/githubREST.mjs";
20 import {
21   buildSDKPath,
22   buildToolchainPath,
23   downloadAndInstallSDK,
24   downloadAndInstallToolchain,
25 } from "../utils/download.mjs";
26
27 enum BoardType {
28   pico = "Pico",
29   picoW = "Pico W",
30 }
31
32 enum ConsoleOption {
33   consoleOverUART = "Console over UART",
34   consoleOverUSB = "Console over USB (disables other USB use)",
35 }
36
37 enum Library {
38   spi = "SPI",
39   i2c = "I2C",
40   dma = "DMA support",
41   pio = "PIO",
42   interp = "HW interpolation",
43   timer = "HW timer",
44   watch = "HW watchdog",
45   clocks = "HW clocks",
46 }
47
48 enum PicoWirelessOption {
49   none = "None",
50   picoWLed = "Pico W onboard LED",
51   picoWPoll = "Polled lwIP",
52   picoWBackground = "Background lwIP",
53 }
54
55 enum CodeOption {
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)",
61 }
62
63 enum Debugger {
64   debugProbe = "DebugProbe (CMSIS-DAP) [Default]",
65   swd = "SWD (Pi host)",
66 }
67
68 function enumToParam(
69   e:
70     | BoardType
71     | ConsoleOption
72     | Library
73     | PicoWirelessOption
74     | CodeOption
75     | Debugger
76 ): string {
77   switch (e) {
78     case BoardType.pico:
79       return "-board pico";
80     case BoardType.picoW:
81       return "-board pico_w";
82     case ConsoleOption.consoleOverUART:
83       return "-uart";
84     case ConsoleOption.consoleOverUSB:
85       return "-usb";
86     case Library.spi:
87       return "-f spi";
88     case Library.i2c:
89       return "-f i2c";
90     case Library.dma:
91       return "-f dma";
92     case Library.pio:
93       return "-f pio";
94     case Library.interp:
95       return "-f interp";
96     case Library.timer:
97       return "-f timer";
98     case Library.watch:
99       return "-f watch";
100     case Library.clocks:
101       return "-f clocks";
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:
109       return "-x";
110     case CodeOption.runFromRAM:
111       return "-r";
112     case CodeOption.cpp:
113       return "-cpp";
114     case CodeOption.cppRtti:
115       return "-cpprtti";
116     case CodeOption.cppExceptions:
117       return "-cppex";
118     case Debugger.debugProbe:
119       return "-d 0";
120     case Debugger.swd:
121       return "-d 1";
122     default:
123       // TODO: maybe just return an empty string
124       throw new Error(`Unknown enum value: ${e as string}`);
125   }
126 }
127
128 interface NewProjectOptions {
129   name: string;
130   projectRoot: string;
131   boardType: BoardType;
132   consoleOptions: ConsoleOption[];
133   libraries: Array<Library | PicoWirelessOption>;
134   codeOptions: CodeOption[];
135   debugger: Debugger;
136   toolchainAndSDK: {
137     toolchainVersion: string;
138     toolchainPath: string;
139     sdkVersion: string;
140     sdkPath: string;
141   };
142 }
143
144 function getScriptsRoot(): string {
145   return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
146 }
147
148 export default class NewProjectCommand extends Command {
149   private readonly _logger: Logger = new Logger("NewProjectCommand");
150   private readonly _settings: Settings;
151
152   constructor(settings: Settings) {
153     super("newProject");
154
155     this._settings = settings;
156   }
157
158   private async selectSDKAndToolchain(): Promise<
159     | {
160         toolchainVersion: string;
161         toolchainPath: string;
162         sdkVersion: string;
163         sdkPath: string;
164       }
165     | undefined
166   > {
167     const installedSDKs = detectInstalledSDKs();
168     const installedToolchains = detectInstalledToolchains();
169
170     try {
171       if (installedSDKs.length === 0 || installedToolchains.length === 0) {
172         // TODO: add offline handling
173         const availableSDKs = await getSDKReleases();
174         const supportedToolchains = await getSupportedToolchains();
175
176         // show quick pick for sdk and toolchain
177         const selectedSDK = await window.showQuickPick(
178           availableSDKs.map(sdk => ({
179             label: `v${sdk.tagName}`,
180             sdk: sdk,
181           })),
182           {
183             placeHolder: "Select Pico SDK version",
184             title: "New Pico Project",
185           }
186         );
187
188         if (selectedSDK === undefined) {
189           return;
190         }
191
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,
197           })),
198           {
199             placeHolder: "Select ARM Embeded Toolchain version",
200             title: "New Pico Project",
201           }
202         );
203
204         if (selectedToolchain === undefined) {
205           return;
206         }
207
208         // show user feedback as downloads can take a while
209         let installedSuccessfully = false;
210         await window.withProgress(
211           {
212             location: ProgressLocation.Notification,
213             title: "Downloading SDK and Toolchain",
214             cancellable: false,
215           },
216           async progress => {
217             // download both
218             if (
219               !(await downloadAndInstallSDK(
220                 selectedSDK.sdk.tagName,
221                 SDK_REPOSITORY_URL
222               )) ||
223               !(await downloadAndInstallToolchain(selectedToolchain.toolchain))
224             ) {
225               Logger.log(`Failed to download and install toolchain and SDK.`);
226
227               progress.report({
228                 message:
229                   "Failed to download and install toolchain and sdk. " +
230                   "Make sure all requirements are met.",
231                 increment: 100,
232               });
233
234               installedSuccessfully = false;
235             } else {
236               installedSuccessfully = true;
237             }
238           }
239         );
240
241         if (!installedSuccessfully) {
242           return;
243         } else {
244           return {
245             toolchainVersion: selectedToolchain.toolchain.version,
246             toolchainPath: buildToolchainPath(
247               selectedToolchain.toolchain.version
248             ),
249             sdkVersion: selectedSDK.sdk.tagName,
250             sdkPath: buildSDKPath(selectedSDK.sdk.tagName),
251           };
252         }
253       }
254
255       // sort installed sdks from newest to oldest
256       installedSDKs.sort((a, b) =>
257         compare(a.version.replace("v", ""), b.version.replace("v", ""))
258       );
259       // toolchains should be sorted and cant be sorted by compare because
260       // of their alphanumeric structure
261
262       return {
263         toolchainVersion: installedToolchains[0].version,
264         toolchainPath: installedToolchains[0].path,
265         sdkVersion: installedSDKs[0].version,
266         sdkPath: installedSDKs[0].sdkPath,
267       };
268     } catch (error) {
269       Logger.log(
270         `Error while retrieving SDK and toolchain versions: ${
271           error instanceof Error ? error.message : (error as string)
272         }`
273       );
274
275       void window.showErrorMessage(
276         "Error while retrieving SDK and toolchain versions."
277       );
278
279       return;
280     }
281   }
282
283   async execute(): Promise<void> {
284     // check if all requirements are met
285     const requirementsCheck = await checkForRequirements(this._settings);
286     if (!requirementsCheck[0]) {
287       void showRequirementsNotMetErrorMessage(requirementsCheck[1]);
288
289       return;
290     }
291
292     // TODO: maybe make it posible to also select a folder and
293     // not always create a new one with selectedName
294     const projectRoot: Uri[] | undefined = await window.showOpenDialog({
295       canSelectFiles: false,
296       canSelectFolders: true,
297       canSelectMany: false,
298       openLabel: "Select project root",
299     });
300
301     // user focused out of the quick pick
302     if (!projectRoot || projectRoot.length !== 1) {
303       return;
304     }
305
306     // get project name
307     const selectedName: string | undefined = await window.showInputBox({
308       placeHolder: "Enter a project name",
309       title: "New Pico Project",
310     });
311
312     // user focused out of the quick pick
313     if (!selectedName) {
314       return;
315     }
316
317     // get board type (single selection)
318     const selectedBoardType: BoardType | undefined =
319       (await window.showQuickPick(Object.values(BoardType), {
320         placeHolder: "Select a board type",
321         title: "New Pico Project",
322       })) as BoardType | undefined;
323
324     // user focused out of the quick pick
325     if (!selectedBoardType) {
326       return;
327     }
328
329     // [optional] get console options (multi selection)
330     const selectedConsoleOptions: ConsoleOption[] | undefined =
331       (await window.showQuickPick(Object.values(ConsoleOption), {
332         placeHolder: "Would you like to enable the USB console?",
333         title: "New Pico Project",
334         canPickMany: true,
335       })) as ConsoleOption[] | undefined;
336
337     // user focused out of the quick pick
338     if (!selectedConsoleOptions) {
339       return;
340     }
341
342     // [optional] get libraries (multi selection)
343     const selectedFeatures: Array<Library | PicoWirelessOption> | undefined =
344       (await window.showQuickPick(Object.values(Library), {
345         placeHolder: "Select libraries to include",
346         title: "New Pico Project",
347         canPickMany: true,
348       })) as Library[] | undefined;
349
350     // user focused out of the quick pick
351     if (!selectedFeatures) {
352       return;
353     }
354
355     if (selectedBoardType === BoardType.picoW) {
356       const selectedWirelessFeature: PicoWirelessOption | undefined =
357         (await window.showQuickPick(Object.values(PicoWirelessOption), {
358           placeHolder:
359             "Select wireless features to include or press enter to skip",
360           title: "New Pico Project",
361           canPickMany: false,
362         })) as PicoWirelessOption | undefined;
363
364       // user focused out of the quick pick
365       if (!selectedWirelessFeature) {
366         return;
367       }
368
369       if (
370         selectedWirelessFeature &&
371         selectedWirelessFeature !== PicoWirelessOption.none
372       ) {
373         selectedFeatures.push(selectedWirelessFeature);
374       }
375     }
376
377     const selectedCodeOptions: CodeOption[] | undefined =
378       (await window.showQuickPick(Object.values(CodeOption), {
379         placeHolder: "Select code generator options to use",
380         title: "New Pico Project",
381         canPickMany: true,
382       })) as CodeOption[] | undefined;
383
384     if (!selectedCodeOptions) {
385       return;
386     }
387
388     const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
389       Object.values(Debugger),
390       {
391         placeHolder: "Select debugger to use",
392         title: "New Pico Project",
393         canPickMany: false,
394       }
395     )) as Debugger | undefined;
396
397     if (!selectedDebugger) {
398       return;
399     }
400
401     const selectedToolchainAndSDK = await this.selectSDKAndToolchain();
402
403     if (selectedToolchainAndSDK === undefined) {
404       return;
405     }
406
407     void window.showWarningMessage(
408       "Generating project, this may take a while..."
409     );
410
411     await this.executePicoProjectGenerator({
412       name: selectedName,
413       projectRoot: projectRoot[0].fsPath,
414       boardType: selectedBoardType,
415       consoleOptions: selectedConsoleOptions,
416       libraries: selectedFeatures,
417       codeOptions: selectedCodeOptions,
418       debugger: selectedDebugger,
419       toolchainAndSDK: selectedToolchainAndSDK,
420     });
421   }
422
423   private runGenerator(
424     command: string,
425     options: ExecOptions
426   ): Promise<number | null> {
427     return new Promise<number | null>(resolve => {
428       const generatorProcess = exec(command, options, error => {
429         if (error) {
430           console.error(`Error: ${error.message}`);
431           resolve(null); // Indicate error
432         }
433       });
434
435       generatorProcess.on("exit", code => {
436         // Resolve with exit code or -1 if code is undefined
437         resolve(code);
438       });
439     });
440   }
441
442   /**
443    * Executes the Pico Project Generator with the given options.
444    *
445    * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
446    */
447   private async executePicoProjectGenerator(
448     options: NewProjectOptions
449   ): Promise<void> {
450     const customEnv: { [key: string]: string } = {
451       ...(process.env as { [key: string]: string }),
452       // set PICO_SDK_PATH
453       ["PICO_SDK_PATH"]: options.toolchainAndSDK.sdkPath,
454       // set PICO_TOOLCHAIN_PATH i needed someday
455       ["PICO_TOOLCHAIN_PATH"]: options.toolchainAndSDK.toolchainPath,
456     };
457     // add compiler to PATH
458     const isWindows = process.platform === "win32";
459     customEnv[isWindows ? "Path" : "PATH"] = `${join(
460       options.toolchainAndSDK.toolchainPath,
461       "bin"
462     )}${isWindows ? ";" : ":"}${customEnv[isWindows ? "Path" : "PATH"]}`;
463     const pythonExe =
464       this._settings.getString(SettingsKey.python3Path) || isWindows
465         ? "python"
466         : "python3";
467
468     const command: string = [
469       pythonExe,
470       join(getScriptsRoot(), "pico_project.py"),
471       enumToParam(options.boardType),
472       ...options.consoleOptions.map(option => enumToParam(option)),
473       !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
474         ? "-nouart"
475         : "",
476       ...options.libraries.map(option => enumToParam(option)),
477       ...options.codeOptions.map(option => enumToParam(option)),
478       enumToParam(options.debugger),
479       // generate .vscode config
480       "--project",
481       "vscode",
482       "--projectRoot",
483       `"${options.projectRoot}"`,
484       "--sdkVersion",
485       options.toolchainAndSDK.sdkVersion,
486       "--toolchainVersion",
487       options.toolchainAndSDK.toolchainVersion,
488       options.name,
489     ].join(" ");
490
491     this._logger.debug(`Executing project generator command: ${command}`);
492
493     // execute command
494     // TODO: use exit codes to determine why the project generator failed (if it did)
495     // to be able to show the user a more detailed error message
496     const generatorExitCode = await this.runGenerator(command, {
497       env: customEnv,
498       cwd: getScriptsRoot(),
499       windowsHide: true,
500       timeout: 15000,
501     });
502     if (generatorExitCode === 0) {
503       void window.showInformationMessage(
504         `Successfully generated new project: ${options.name}`
505       );
506
507       // open new folder
508       void commands.executeCommand(
509         "vscode.openFolder",
510         Uri.file(join(options.projectRoot, options.name)),
511         (workspace.workspaceFolders?.length ?? 0) > 0
512       );
513     } else {
514       this._logger.error(
515         `Generator Process exited with code: ${generatorExitCode ?? "null"}`
516       );
517
518       void window.showErrorMessage(
519         `Could not create new project: ${options.name}`
520       );
521     }
522   }
523 }
This page took 0.055332 seconds and 4 git commands to generate.