]> Git Repo - pico-vscode.git/blob - src/commands/newProject.mts
Fix pico project generator not detecting path and exit with null on win
[pico-vscode.git] / src / commands / newProject.mts
1 import { 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 {
15   generateNewEnvVarSuffix,
16   setGlobalEnvVar,
17 } from "../utils/globalEnvironmentUtil.mjs";
18 import { compare } from "../utils/semverUtil.mjs";
19
20 enum BoardType {
21   pico = "Pico",
22   picoW = "Pico W",
23 }
24
25 enum ConsoleOption {
26   consoleOverUART = "Console over UART",
27   consoleOverUSB = "Console over USB (disables other USB use)",
28 }
29
30 enum Library {
31   spi = "SPI",
32   i2c = "I2C",
33   dma = "DMA support",
34   pio = "PIO",
35   interp = "HW interpolation",
36   timer = "HW timer",
37   watch = "HW watchdog",
38   clocks = "HW clocks",
39 }
40
41 enum PicoWirelessOption {
42   none = "None",
43   picoWLed = "Pico W onboard LED",
44   picoWPoll = "Polled lwIP",
45   picoWBackground = "Background lwIP",
46 }
47
48 enum CodeOption {
49   addExamples = "Add examples from Pico library",
50   runFromRAM = "Run the program from RAM rather than flash",
51   cpp = "Generate C++ code",
52   cppRtti = "Enable C++ RTTI (Uses more memory)",
53   cppExceptions = "Enable C++ exceptions (Uses more memory)",
54 }
55
56 enum Debugger {
57   debugProbe = "DebugProbe (CMSIS-DAP) [Default]",
58   swd = "SWD (Pi host)",
59 }
60
61 function enumToParam(
62   e:
63     | BoardType
64     | ConsoleOption
65     | Library
66     | PicoWirelessOption
67     | CodeOption
68     | Debugger
69 ): string {
70   switch (e) {
71     case BoardType.pico:
72       return "-board pico";
73     case BoardType.picoW:
74       return "-board pico_w";
75     case ConsoleOption.consoleOverUART:
76       return "-uart";
77     case ConsoleOption.consoleOverUSB:
78       return "-usb";
79     case Library.spi:
80       return "-f spi";
81     case Library.i2c:
82       return "-f i2c";
83     case Library.dma:
84       return "-f dma";
85     case Library.pio:
86       return "-f pio";
87     case Library.interp:
88       return "-f interp";
89     case Library.timer:
90       return "-f timer";
91     case Library.watch:
92       return "-f watch";
93     case Library.clocks:
94       return "-f clocks";
95     case PicoWirelessOption.picoWLed:
96       return "-f picow_led";
97     case PicoWirelessOption.picoWPoll:
98       return "-f picow_poll";
99     case PicoWirelessOption.picoWBackground:
100       return "-f picow_background";
101     case CodeOption.addExamples:
102       return "-x";
103     case CodeOption.runFromRAM:
104       return "-r";
105     case CodeOption.cpp:
106       return "-cpp";
107     case CodeOption.cppRtti:
108       return "-cpprtti";
109     case CodeOption.cppExceptions:
110       return "-cppex";
111     case Debugger.debugProbe:
112       return "-d 0";
113     case Debugger.swd:
114       return "-d 1";
115     default:
116       // TODO: maybe just return an empty string
117       throw new Error(`Unknown enum value: ${e as string}`);
118   }
119 }
120
121 interface NewProjectOptions {
122   name: string;
123   projectRoot: string;
124   boardType: BoardType;
125   consoleOptions: ConsoleOption[];
126   libraries: Array<Library | PicoWirelessOption>;
127   codeOptions: CodeOption[];
128   debugger: Debugger;
129 }
130
131 function getScriptsRoot(): string {
132   return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
133 }
134
135 export default class NewProjectCommand extends Command {
136   private readonly _logger: Logger = new Logger("NewProjectCommand");
137   private readonly _settings: Settings;
138
139   constructor(settings: Settings) {
140     super("newProject");
141
142     this._settings = settings;
143   }
144
145   async execute(): Promise<void> {
146     // check if all requirements are met
147     if (!(await checkForRequirements(this._settings))) {
148       void showRquirementsNotMetErrorMessage();
149
150       return;
151     }
152
153     // TODO: maybe make it posible to also select a folder and
154     // not always create a new one with selectedName
155     const projectRoot: Uri[] | undefined = await window.showOpenDialog({
156       canSelectFiles: false,
157       canSelectFolders: true,
158       canSelectMany: false,
159       openLabel: "Select project root",
160     });
161
162     // user focused out of the quick pick
163     if (!projectRoot || projectRoot.length !== 1) {
164       return;
165     }
166
167     // get project name
168     const selectedName: string | undefined = await window.showInputBox({
169       placeHolder: "Enter a project name",
170       title: "New Pico Project",
171     });
172
173     // user focused out of the quick pick
174     if (!selectedName) {
175       return;
176     }
177
178     // get board type (single selection)
179     const selectedBoardType: BoardType | undefined =
180       (await window.showQuickPick(Object.values(BoardType), {
181         placeHolder: "Select a board type",
182         title: "New Pico Project",
183       })) as BoardType | undefined;
184
185     // user focused out of the quick pick
186     if (!selectedBoardType) {
187       return;
188     }
189
190     // [optional] get console options (multi selection)
191     const selectedConsoleOptions: ConsoleOption[] | undefined =
192       (await window.showQuickPick(Object.values(ConsoleOption), {
193         placeHolder: "Would you like to enable the USB console?",
194         title: "New Pico Project",
195         canPickMany: true,
196       })) as ConsoleOption[] | undefined;
197
198     // user focused out of the quick pick
199     if (!selectedConsoleOptions) {
200       return;
201     }
202
203     // [optional] get libraries (multi selection)
204     const selectedFeatures: Array<Library | PicoWirelessOption> | undefined =
205       (await window.showQuickPick(Object.values(Library), {
206         placeHolder: "Select libraries to include",
207         title: "New Pico Project",
208         canPickMany: true,
209       })) as Library[] | undefined;
210
211     // user focused out of the quick pick
212     if (!selectedFeatures) {
213       return;
214     }
215
216     if (selectedBoardType === BoardType.picoW) {
217       const selectedWirelessFeature: PicoWirelessOption | undefined =
218         (await window.showQuickPick(Object.values(PicoWirelessOption), {
219           placeHolder:
220             "Select wireless features to include or press enter to skip",
221           title: "New Pico Project",
222           canPickMany: false,
223         })) as PicoWirelessOption | undefined;
224
225       // user focused out of the quick pick
226       if (!selectedWirelessFeature) {
227         return;
228       }
229
230       if (
231         selectedWirelessFeature &&
232         selectedWirelessFeature !== PicoWirelessOption.none
233       ) {
234         selectedFeatures.push(selectedWirelessFeature);
235       }
236     }
237
238     const selectedCodeOptions: CodeOption[] | undefined =
239       (await window.showQuickPick(Object.values(CodeOption), {
240         placeHolder: "Select code generator options to use",
241         title: "New Pico Project",
242         canPickMany: true,
243       })) as CodeOption[] | undefined;
244
245     if (!selectedCodeOptions) {
246       return;
247     }
248
249     const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
250       Object.values(Debugger),
251       {
252         placeHolder: "Select debugger to use",
253         title: "New Pico Project",
254         canPickMany: false,
255       }
256     )) as Debugger | undefined;
257
258     if (!selectedDebugger) {
259       return;
260     }
261
262     void window.showWarningMessage(
263       "Generating project, this may take a while. " +
264         "For linting and auto-complete to work, " +
265         "please completly restart VSCode after the project has been generated."
266     );
267
268     await this.executePicoProjectGenerator({
269       name: selectedName,
270       projectRoot: projectRoot[0].fsPath,
271       boardType: selectedBoardType,
272       consoleOptions: selectedConsoleOptions,
273       libraries: selectedFeatures,
274       codeOptions: selectedCodeOptions,
275       debugger: selectedDebugger,
276     });
277   }
278
279   private runGenerator(
280     command: string,
281     options: ExecOptions
282   ): Promise<number | null> {
283     return new Promise<number | null>(resolve => {
284       const generatorProcess = exec(command, options, error => {
285         if (error) {
286           console.error(`Error: ${error.message}`);
287           resolve(null); // Indicate error
288         }
289       });
290
291       generatorProcess.on("exit", code => {
292         // Resolve with exit code or -1 if code is undefined
293         resolve(code);
294       });
295     });
296   }
297
298   /**
299    * Executes the Pico Project Generator with the given options
300    *
301    * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
302    */
303   private async executePicoProjectGenerator(
304     options: NewProjectOptions
305   ): Promise<void> {
306     /*const [PICO_SDK_PATH, COMPILER_PATH] = (await getSDKAndToolchainPath(
307       this._settings
308     )) ?? [];*/
309     const installedSDKs = detectInstalledSDKs().sort((a, b) =>
310       compare(a.version.replace("v", ""), b.version.replace("v", ""))
311     );
312
313     if (
314       installedSDKs.length === 0 ||
315       // "protection" against empty settings
316       installedSDKs[0].sdkPath === "" ||
317       installedSDKs[0].toolchainPath === ""
318     ) {
319       void window.showErrorMessage(
320         "Could not find Pico SDK or Toolchain. Please check the wiki."
321       );
322
323       return;
324     }
325
326     const PICO_SDK_PATH = installedSDKs[0].sdkPath;
327     const TOOLCHAIN_PATH = installedSDKs[0].toolchainPath;
328     const ENV_SUFFIX = generateNewEnvVarSuffix();
329     setGlobalEnvVar(`PICO_SDK_PATH_${ENV_SUFFIX}`, PICO_SDK_PATH);
330     setGlobalEnvVar(`PICO_TOOLCHAIN_PATH_${ENV_SUFFIX}`, TOOLCHAIN_PATH);
331
332     const customEnv: { [key: string]: string } = {
333       ...(process.env as { [key: string]: string }),
334       // set PICO_SDK_PATH
335       ["PICO_SDK_PATH"]: PICO_SDK_PATH,
336       // set PICO_TOOLCHAIN_PATH i needed someday
337       ["PICO_TOOLCHAIN_PATH"]: TOOLCHAIN_PATH,
338
339       // if project generator compiles the project, it needs the suffixed env vars
340       // not requiret any more because of process.env above
341       // eslint-disable-next-line @typescript-eslint/naming-convention
342       [`PICO_SDK_PATH_${ENV_SUFFIX}`]: PICO_SDK_PATH,
343       // eslint-disable-next-line @typescript-eslint/naming-convention
344       [`PICO_TOOLCHAIN_PATH_${ENV_SUFFIX}`]: TOOLCHAIN_PATH,
345     };
346     // add compiler to PATH
347     customEnv[
348       process.platform === "win32" ? "Path" : "PATH"
349     ] = `${TOOLCHAIN_PATH};${
350       customEnv[process.platform === "win32" ? "Path" : "PATH"]
351     }`;
352     const pythonExe =
353       this._settings.getString(SettingsKey.python3Path) ||
354       process.platform === "win32"
355         ? "python"
356         : "python3";
357
358     const command: string = [
359       pythonExe,
360       join(getScriptsRoot(), "pico_project.py"),
361       enumToParam(options.boardType),
362       ...options.consoleOptions.map(option => enumToParam(option)),
363       !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
364         ? "-nouart"
365         : "",
366       ...options.libraries.map(option => enumToParam(option)),
367       ...options.codeOptions.map(option => enumToParam(option)),
368       enumToParam(options.debugger),
369       // generate .vscode config
370       "--project",
371       "vscode",
372       "--projectRoot",
373       `"${options.projectRoot}"`,
374       "--envSuffix",
375       ENV_SUFFIX,
376       "--sdkVersion",
377       installedSDKs[0].version,
378       options.name,
379     ].join(" ");
380
381     this._logger.debug(`Executing project generator command: ${command}`);
382
383     // execute command
384     // TODO: use exit codes to determine why the project generator failed (if it did)
385     // to be able to show the user a more detailed error message
386     const generatorExitCode = await this.runGenerator(command, {
387       env: customEnv,
388       cwd: getScriptsRoot(),
389       windowsHide: true,
390       timeout: 15000,
391     });
392     if (generatorExitCode === 0) {
393       void window.showInformationMessage(
394         `Successfully created project: ${options.name}`
395       );
396
397       // open new folder
398       void commands.executeCommand(
399         "vscode.openFolder",
400         Uri.file(join(options.projectRoot, options.name)),
401         (workspace.workspaceFolders?.length ?? 0) > 0
402       );
403     } else {
404       this._logger.error(
405         `Generator Process exited with code: ${generatorExitCode ?? "null"}`
406       );
407
408       void window.showErrorMessage(`Could not create project ${options.name}`);
409     }
410   }
411 }
This page took 0.05312 seconds and 4 git commands to generate.