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";
11 showRquirementsNotMetErrorMessage,
12 } from "../utils/requirementsUtil.mjs";
13 import { SettingsKey } from "../settings.mjs";
15 generateNewEnvVarSuffix,
17 } from "../utils/globalEnvironmentUtil.mjs";
18 import { compare } from "../utils/semverUtil.mjs";
26 consoleOverUART = "Console over UART",
27 consoleOverUSB = "Console over USB (disables other USB use)",
35 interp = "HW interpolation",
37 watch = "HW watchdog",
41 enum PicoWirelessOption {
43 picoWLed = "Pico W onboard LED",
44 picoWPoll = "Polled lwIP",
45 picoWBackground = "Background lwIP",
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)",
57 debugProbe = "DebugProbe (CMSIS-DAP) [Default]",
58 swd = "SWD (Pi host)",
74 return "-board pico_w";
75 case ConsoleOption.consoleOverUART:
77 case ConsoleOption.consoleOverUSB:
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:
103 case CodeOption.runFromRAM:
107 case CodeOption.cppRtti:
109 case CodeOption.cppExceptions:
111 case Debugger.debugProbe:
116 // TODO: maybe just return an empty string
117 throw new Error(`Unknown enum value: ${e as string}`);
121 interface NewProjectOptions {
124 boardType: BoardType;
125 consoleOptions: ConsoleOption[];
126 libraries: Array<Library | PicoWirelessOption>;
127 codeOptions: CodeOption[];
131 function getScriptsRoot(): string {
132 return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
135 export default class NewProjectCommand extends Command {
136 private readonly _logger: Logger = new Logger("NewProjectCommand");
137 private readonly _settings: Settings;
139 constructor(settings: Settings) {
142 this._settings = settings;
145 async execute(): Promise<void> {
146 // check if all requirements are met
147 if (!(await checkForRequirements(this._settings))) {
148 void showRquirementsNotMetErrorMessage();
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",
162 // user focused out of the quick pick
163 if (!projectRoot || projectRoot.length !== 1) {
168 const selectedName: string | undefined = await window.showInputBox({
169 placeHolder: "Enter a project name",
170 title: "New Pico Project",
173 // user focused out of the quick pick
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;
185 // user focused out of the quick pick
186 if (!selectedBoardType) {
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",
196 })) as ConsoleOption[] | undefined;
198 // user focused out of the quick pick
199 if (!selectedConsoleOptions) {
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",
209 })) as Library[] | undefined;
211 // user focused out of the quick pick
212 if (!selectedFeatures) {
216 if (selectedBoardType === BoardType.picoW) {
217 const selectedWirelessFeature: PicoWirelessOption | undefined =
218 (await window.showQuickPick(Object.values(PicoWirelessOption), {
220 "Select wireless features to include or press enter to skip",
221 title: "New Pico Project",
223 })) as PicoWirelessOption | undefined;
225 // user focused out of the quick pick
226 if (!selectedWirelessFeature) {
231 selectedWirelessFeature &&
232 selectedWirelessFeature !== PicoWirelessOption.none
234 selectedFeatures.push(selectedWirelessFeature);
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",
243 })) as CodeOption[] | undefined;
245 if (!selectedCodeOptions) {
249 const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
250 Object.values(Debugger),
252 placeHolder: "Select debugger to use",
253 title: "New Pico Project",
256 )) as Debugger | undefined;
258 if (!selectedDebugger) {
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."
268 await this.executePicoProjectGenerator({
270 projectRoot: projectRoot[0].fsPath,
271 boardType: selectedBoardType,
272 consoleOptions: selectedConsoleOptions,
273 libraries: selectedFeatures,
274 codeOptions: selectedCodeOptions,
275 debugger: selectedDebugger,
279 private runGenerator(
282 ): Promise<number | null> {
283 return new Promise<number | null>(resolve => {
284 const generatorProcess = exec(command, options, error => {
286 console.error(`Error: ${error.message}`);
287 resolve(null); // Indicate error
291 generatorProcess.on("exit", code => {
292 // Resolve with exit code or -1 if code is undefined
299 * Executes the Pico Project Generator with the given options
301 * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
303 private async executePicoProjectGenerator(
304 options: NewProjectOptions
306 /*const [PICO_SDK_PATH, COMPILER_PATH] = (await getSDKAndToolchainPath(
309 const installedSDKs = detectInstalledSDKs().sort((a, b) =>
310 compare(a.version.replace("v", ""), b.version.replace("v", ""))
314 installedSDKs.length === 0 ||
315 // "protection" against empty settings
316 installedSDKs[0].sdkPath === "" ||
317 installedSDKs[0].toolchainPath === ""
319 void window.showErrorMessage(
320 "Could not find Pico SDK or Toolchain. Please check the wiki."
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);
332 const customEnv: { [key: string]: string } = {
333 ...(process.env as { [key: string]: string }),
335 ["PICO_SDK_PATH"]: PICO_SDK_PATH,
336 // set PICO_TOOLCHAIN_PATH i needed someday
337 ["PICO_TOOLCHAIN_PATH"]: TOOLCHAIN_PATH,
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,
346 // add compiler to PATH
348 process.platform === "win32" ? "Path" : "PATH"
349 ] = `${TOOLCHAIN_PATH};${
350 customEnv[process.platform === "win32" ? "Path" : "PATH"]
353 this._settings.getString(SettingsKey.python3Path) ||
354 process.platform === "win32"
358 const command: string = [
360 join(getScriptsRoot(), "pico_project.py"),
361 enumToParam(options.boardType),
362 ...options.consoleOptions.map(option => enumToParam(option)),
363 !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
366 ...options.libraries.map(option => enumToParam(option)),
367 ...options.codeOptions.map(option => enumToParam(option)),
368 enumToParam(options.debugger),
369 // generate .vscode config
373 `"${options.projectRoot}"`,
377 installedSDKs[0].version,
381 this._logger.debug(`Executing project generator command: ${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, {
388 cwd: getScriptsRoot(),
392 if (generatorExitCode === 0) {
393 void window.showInformationMessage(
394 `Successfully created project: ${options.name}`
398 void commands.executeCommand(
400 Uri.file(join(options.projectRoot, options.name)),
401 (workspace.workspaceFolders?.length ?? 0) > 0
405 `Generator Process exited with code: ${generatorExitCode ?? "null"}`
408 void window.showErrorMessage(`Could not create project ${options.name}`);