]> Git Repo - pico-vscode.git/blob - src/commands/newProject.mts
Replaced global-env with sdk-toolchain management
[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   showRquirementsNotMetErrorMessage,
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 { 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                 selectedSDK.sdk.downloadUrl
222               )) ||
223               !(await downloadAndInstallToolchain(selectedToolchain.toolchain))
224             ) {
225               Logger.log(`Failed to download and install toolchain and SDK.`);
226
227               progress.report({
228                 message: "Failed to download and install toolchain and sdk.",
229                 increment: 100,
230               });
231
232               installedSuccessfully = false;
233             } else {
234               installedSuccessfully = true;
235             }
236           }
237         );
238
239         if (!installedSuccessfully) {
240           return;
241         } else {
242           return {
243             toolchainVersion: selectedToolchain.toolchain.version,
244             toolchainPath: buildToolchainPath(
245               selectedToolchain.toolchain.version
246             ),
247             sdkVersion: selectedSDK.sdk.tagName,
248             sdkPath: buildSDKPath(selectedSDK.sdk.tagName),
249           };
250         }
251       }
252
253       // sort installed sdks from newest to oldest
254       installedSDKs.sort((a, b) =>
255         compare(a.version.replace("v", ""), b.version.replace("v", ""))
256       );
257       // toolchains should be sorted and cant be sorted by compare because
258       // of their alphanumeric structure
259
260       return {
261         toolchainVersion: installedToolchains[0].version,
262         toolchainPath: installedToolchains[0].path,
263         sdkVersion: installedSDKs[0].version,
264         sdkPath: installedSDKs[0].sdkPath,
265       };
266     } catch (error) {
267       Logger.log(
268         `Error while retrieving SDK and toolchain versions: ${
269           error instanceof Error ? error.message : (error as string)
270         }`
271       );
272
273       void window.showErrorMessage(
274         "Error while retrieving SDK and toolchain versions."
275       );
276
277       return;
278     }
279   }
280
281   async execute(): Promise<void> {
282     // check if all requirements are met
283     if (!(await checkForRequirements(this._settings))) {
284       void showRquirementsNotMetErrorMessage();
285
286       return;
287     }
288
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",
296     });
297
298     // user focused out of the quick pick
299     if (!projectRoot || projectRoot.length !== 1) {
300       return;
301     }
302
303     // get project name
304     const selectedName: string | undefined = await window.showInputBox({
305       placeHolder: "Enter a project name",
306       title: "New Pico Project",
307     });
308
309     // user focused out of the quick pick
310     if (!selectedName) {
311       return;
312     }
313
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;
320
321     // user focused out of the quick pick
322     if (!selectedBoardType) {
323       return;
324     }
325
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",
331         canPickMany: true,
332       })) as ConsoleOption[] | undefined;
333
334     // user focused out of the quick pick
335     if (!selectedConsoleOptions) {
336       return;
337     }
338
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",
344         canPickMany: true,
345       })) as Library[] | undefined;
346
347     // user focused out of the quick pick
348     if (!selectedFeatures) {
349       return;
350     }
351
352     if (selectedBoardType === BoardType.picoW) {
353       const selectedWirelessFeature: PicoWirelessOption | undefined =
354         (await window.showQuickPick(Object.values(PicoWirelessOption), {
355           placeHolder:
356             "Select wireless features to include or press enter to skip",
357           title: "New Pico Project",
358           canPickMany: false,
359         })) as PicoWirelessOption | undefined;
360
361       // user focused out of the quick pick
362       if (!selectedWirelessFeature) {
363         return;
364       }
365
366       if (
367         selectedWirelessFeature &&
368         selectedWirelessFeature !== PicoWirelessOption.none
369       ) {
370         selectedFeatures.push(selectedWirelessFeature);
371       }
372     }
373
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",
378         canPickMany: true,
379       })) as CodeOption[] | undefined;
380
381     if (!selectedCodeOptions) {
382       return;
383     }
384
385     const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
386       Object.values(Debugger),
387       {
388         placeHolder: "Select debugger to use",
389         title: "New Pico Project",
390         canPickMany: false,
391       }
392     )) as Debugger | undefined;
393
394     if (!selectedDebugger) {
395       return;
396     }
397
398     const selectedToolchainAndSDK = await this.selectSDKAndToolchain();
399
400     if (selectedToolchainAndSDK === undefined) {
401       return;
402     }
403
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."
408     );
409
410     await this.executePicoProjectGenerator({
411       name: selectedName,
412       projectRoot: projectRoot[0].fsPath,
413       boardType: selectedBoardType,
414       consoleOptions: selectedConsoleOptions,
415       libraries: selectedFeatures,
416       codeOptions: selectedCodeOptions,
417       debugger: selectedDebugger,
418       toolchainAndSDK: selectedToolchainAndSDK,
419     });
420   }
421
422   private runGenerator(
423     command: string,
424     options: ExecOptions
425   ): Promise<number | null> {
426     return new Promise<number | null>(resolve => {
427       const generatorProcess = exec(command, options, error => {
428         if (error) {
429           console.error(`Error: ${error.message}`);
430           resolve(null); // Indicate error
431         }
432       });
433
434       generatorProcess.on("exit", code => {
435         // Resolve with exit code or -1 if code is undefined
436         resolve(code);
437       });
438     });
439   }
440
441   /**
442    * Executes the Pico Project Generator with the given options.
443    *
444    * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
445    */
446   private async executePicoProjectGenerator(
447     options: NewProjectOptions
448   ): Promise<void> {
449     const customEnv: { [key: string]: string } = {
450       ...(process.env as { [key: string]: string }),
451       // set PICO_SDK_PATH
452       ["PICO_SDK_PATH"]: options.toolchainAndSDK.sdkPath,
453       // set PICO_TOOLCHAIN_PATH i needed someday
454       ["PICO_TOOLCHAIN_PATH"]: options.toolchainAndSDK.toolchainPath,
455     };
456     // add compiler to PATH
457     const isWindows = process.platform === "win32";
458     customEnv[isWindows ? "Path" : "PATH"] = `${join(
459       options.toolchainAndSDK.toolchainPath,
460       "bin"
461     )}${isWindows ? ";" : ":"}${customEnv[isWindows ? "Path" : "PATH"]}`;
462     const pythonExe =
463       this._settings.getString(SettingsKey.python3Path) || isWindows
464         ? "python"
465         : "python3";
466
467     const command: string = [
468       pythonExe,
469       join(getScriptsRoot(), "pico_project.py"),
470       enumToParam(options.boardType),
471       ...options.consoleOptions.map(option => enumToParam(option)),
472       !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
473         ? "-nouart"
474         : "",
475       ...options.libraries.map(option => enumToParam(option)),
476       ...options.codeOptions.map(option => enumToParam(option)),
477       enumToParam(options.debugger),
478       // generate .vscode config
479       "--project",
480       "vscode",
481       "--projectRoot",
482       `"${options.projectRoot}"`,
483       "--sdkVersion",
484       options.toolchainAndSDK.sdkVersion,
485       "--toolchainVersion",
486       options.toolchainAndSDK.toolchainVersion,
487       options.name,
488     ].join(" ");
489
490     this._logger.debug(`Executing project generator command: ${command}`);
491
492     // execute 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, {
496       env: customEnv,
497       cwd: getScriptsRoot(),
498       windowsHide: true,
499       timeout: 15000,
500     });
501     if (generatorExitCode === 0) {
502       void window.showInformationMessage(
503         `Successfully generated new project: ${options.name}`
504       );
505
506       // open new folder
507       void commands.executeCommand(
508         "vscode.openFolder",
509         Uri.file(join(options.projectRoot, options.name)),
510         (workspace.workspaceFolders?.length ?? 0) > 0
511       );
512     } else {
513       this._logger.error(
514         `Generator Process exited with code: ${generatorExitCode ?? "null"}`
515       );
516
517       void window.showErrorMessage(
518         `Could not create new project: ${options.name}`
519       );
520     }
521   }
522 }
This page took 0.053805 seconds and 4 git commands to generate.