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