]> Git Repo - pico-vscode.git/blob - src/commands/newProject.mts
90ddda4fd11a222e02651072512e9be3af684105
[pico-vscode.git] / src / commands / newProject.mts
1 import { Uri, commands, window } 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 { redirectVSCodeConfig } from "../utils/vscodeConfigUtil.mjs";
14 import { compare } from "semver";
15 import { SettingsKey } from "../settings.mjs";
16
17 enum BoardType {
18   pico = "Pico",
19   picoW = "Pico W",
20 }
21
22 enum ConsoleOptions {
23   consoleOverUART = "Console over UART",
24   consoleOverUSB = "Console over USB (disables other USB use)",
25 }
26
27 enum Libraries {
28   spi = "SPI",
29   i2c = "I2C",
30   dma = "DMA support",
31   pio = "PIO",
32   interp = "HW interpolation",
33   timer = "HW timer",
34   watch = "HW watchdog",
35   clocks = "HW clocks",
36 }
37
38 function enumToParam(e: BoardType | ConsoleOptions | Libraries): string {
39   switch (e) {
40     case BoardType.pico:
41       return "-board pico";
42     case BoardType.picoW:
43       return "-board pico_w";
44     case ConsoleOptions.consoleOverUART:
45       return "-uart";
46     case ConsoleOptions.consoleOverUSB:
47       return "-usb";
48     case Libraries.spi:
49       return "-f spi";
50     case Libraries.i2c:
51       return "-f i2c";
52     case Libraries.dma:
53       return "-f dma";
54     case Libraries.pio:
55       return "-f pio";
56     case Libraries.interp:
57       return "-f interp";
58     case Libraries.timer:
59       return "-f timer";
60     case Libraries.watch:
61       return "-f watch";
62     case Libraries.clocks:
63       return "-f clocks";
64     default:
65       throw new Error(`Unknown enum value: ${e as string}`);
66   }
67 }
68
69 interface NewProjectOptions {
70   name: string;
71   projectRoot: string;
72   boardType: BoardType;
73   consoleOptions: ConsoleOptions[];
74   libraries: Libraries[];
75 }
76
77 function getScriptsRoot(): string {
78   return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
79 }
80
81 export default class NewProjectCommand extends Command {
82   private readonly _logger: Logger = new Logger("NewProjectCommand");
83   private readonly _settings: Settings;
84
85   constructor(settings: Settings) {
86     super("newProject");
87
88     this._settings = settings;
89   }
90
91   async execute(): Promise<void> {
92     // check if all requirements are met
93     if (!(await checkForRequirements(this._settings))) {
94       void showRquirementsNotMetErrorMessage();
95
96       return;
97     }
98
99     // TODO: maybe make it posible to also select a folder and
100     // not always create a new one with selectedName
101     const projectRoot: Uri[] | undefined = await window.showOpenDialog({
102       canSelectFiles: false,
103       canSelectFolders: true,
104       canSelectMany: false,
105       openLabel: "Select project root",
106     });
107
108     if (!projectRoot || projectRoot.length !== 1) {
109       return;
110     }
111
112     // get project name
113     const selectedName: string | undefined = await window.showInputBox({
114       placeHolder: "Enter a project name",
115       title: "New Pico Project",
116     });
117
118     if (!selectedName) {
119       return;
120     }
121
122     // get board type (single selection)
123     const selectedBoardType: BoardType | undefined =
124       (await window.showQuickPick(Object.values(BoardType), {
125         placeHolder: "Select a board type",
126         title: "New Pico Project",
127       })) as BoardType | undefined;
128
129     if (!selectedBoardType) {
130       return;
131     }
132
133     // [optional] get console options (multi selection)
134     const selectedConsoleOptions: ConsoleOptions[] | undefined =
135       (await window.showQuickPick(Object.values(ConsoleOptions), {
136         placeHolder: "Would you like to enable the USB console?",
137         title: "New Pico Project",
138         canPickMany: true,
139       })) as ConsoleOptions[] | undefined;
140
141     if (!selectedConsoleOptions) {
142       return;
143     }
144
145     // [optional] get libraries (multi selection)
146     const selectedLibraries: Libraries[] | undefined =
147       (await window.showQuickPick(Object.values(Libraries), {
148         placeHolder: "Select libraries to include",
149         title: "New Pico Project",
150         canPickMany: true,
151       })) as Libraries[] | undefined;
152
153     if (!selectedLibraries) {
154       return;
155     }
156
157     await this.executePicoProjectGenerator({
158       name: selectedName,
159       projectRoot: projectRoot[0].fsPath,
160       boardType: selectedBoardType,
161       consoleOptions: selectedConsoleOptions,
162       libraries: selectedLibraries,
163     });
164   }
165
166   private runGenerator(
167     command: string,
168     options: ExecOptions
169   ): Promise<number | null> {
170     return new Promise<number | null>(resolve => {
171       const generatorProcess = exec(command, options, error => {
172         if (error) {
173           console.error(`Error: ${error.message}`);
174           resolve(null); // Indicate error
175         }
176       });
177
178       generatorProcess.on("exit", code => {
179         // Resolve with exit code or -1 if code is undefined
180         resolve(code);
181       });
182     });
183   }
184
185   /**
186    * Executes the Pico Project Generator with the given options
187    *
188    * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
189    */
190   private async executePicoProjectGenerator(
191     options: NewProjectOptions
192   ): Promise<void> {
193     /*const [PICO_SDK_PATH, COMPILER_PATH] = (await getSDKAndToolchainPath(
194       this._settings
195     )) ?? [];*/
196     const installedSDKs = detectInstalledSDKs().sort((a, b) =>
197       compare(a.version, b.version)
198     );
199
200     if (
201       installedSDKs.length === 0 ||
202       // "protection" against empty settings
203       installedSDKs[0].sdkPath === "" ||
204       installedSDKs[0].toolchainPath === ""
205     ) {
206       void window.showErrorMessage(
207         "Could not find Pico SDK or Toolchain. Please check the wiki."
208       );
209
210       return;
211     }
212
213     const PICO_SDK_PATH = installedSDKs[0].sdkPath;
214     const TOOLCHAIN_PATH = installedSDKs[0].toolchainPath;
215
216     const customEnv: { [key: string]: string } = {
217       ...process.env,
218       // eslint-disable-next-line @typescript-eslint/naming-convention
219       PICO_SDK_PATH: PICO_SDK_PATH,
220       // eslint-disable-next-line @typescript-eslint/naming-convention
221       PICO_TOOLCHAIN_PATH: TOOLCHAIN_PATH,
222     };
223     customEnv[
224       process.platform === "win32" ? "Path" : "PATH"
225     ] = `${TOOLCHAIN_PATH}:${
226       customEnv[process.platform === "win32" ? "Path" : "PATH"]
227     }`;
228     const pythonExe =
229       this._settings.getString(SettingsKey.python3Path) ||
230       process.platform === "win32"
231         ? "python"
232         : "python3";
233
234     const command: string = [
235       pythonExe,
236       join(getScriptsRoot(), "pico_project.py"),
237       enumToParam(options.boardType),
238       ...options.consoleOptions.map(option => enumToParam(option)),
239       !options.consoleOptions.includes(ConsoleOptions.consoleOverUART)
240         ? "-nouart"
241         : "",
242       ...options.libraries.map(option => enumToParam(option)),
243       // generate .vscode config
244       "--project",
245       "vscode",
246       "--projectRoot",
247       `"${options.projectRoot}"`,
248       options.name,
249     ].join(" ");
250
251     this._logger.debug(`Executing project generator command: ${command}`);
252
253     // execute command
254     // TODO: use exit codes to determine why the project generator failed (if it did)
255     // to be able to show the user a more detailed error message
256     const generatorExitCode = await this.runGenerator(command, {
257       env: customEnv,
258       cwd: getScriptsRoot(),
259       windowsHide: true,
260       timeout: 5000,
261     });
262     if (generatorExitCode === 0) {
263       await redirectVSCodeConfig(
264         join(options.projectRoot, options.name, ".vscode"),
265         this._settings.getExtensionId(),
266         this._settings.getExtensionName(),
267         PICO_SDK_PATH,
268         TOOLCHAIN_PATH,
269         installedSDKs[0].version
270       );
271       void window.showInformationMessage(
272         `Successfully created project: ${options.name}`
273       );
274       // open new folder
275       void commands.executeCommand(
276         "vscode.openFolder",
277         Uri.file(join(options.projectRoot, options.name)),
278         true
279       );
280     } else {
281       this._logger.error(
282         `Generator Process exited with code: ${generatorExitCode ?? "null"}`
283       );
284
285       void window.showErrorMessage(`Could not create project ${options.name}`);
286     }
287   }
288 }
This page took 0.03398 seconds and 2 git commands to generate.