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";
11 showRequirementsNotMetErrorMessage,
12 } from "../utils/requirementsUtil.mjs";
13 import { SettingsKey } from "../settings.mjs";
14 import { compare } from "../utils/semverUtil.mjs";
16 detectInstalledToolchains,
17 getSupportedToolchains,
18 } from "../utils/toolchainUtil.mjs";
19 import { SDK_REPOSITORY_URL, getSDKReleases } from "../utils/githubREST.mjs";
23 downloadAndInstallSDK,
24 downloadAndInstallToolchain,
25 } from "../utils/download.mjs";
33 consoleOverUART = "Console over UART",
34 consoleOverUSB = "Console over USB (disables other USB use)",
42 interp = "HW interpolation",
44 watch = "HW watchdog",
48 enum PicoWirelessOption {
50 picoWLed = "Pico W onboard LED",
51 picoWPoll = "Polled lwIP",
52 picoWBackground = "Background lwIP",
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)",
64 debugProbe = "DebugProbe (CMSIS-DAP) [Default]",
65 swd = "SWD (Pi host)",
81 return "-board pico_w";
82 case ConsoleOption.consoleOverUART:
84 case ConsoleOption.consoleOverUSB:
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:
110 case CodeOption.runFromRAM:
114 case CodeOption.cppRtti:
116 case CodeOption.cppExceptions:
118 case Debugger.debugProbe:
123 // TODO: maybe just return an empty string
124 throw new Error(`Unknown enum value: ${e as string}`);
128 interface NewProjectOptions {
131 boardType: BoardType;
132 consoleOptions: ConsoleOption[];
133 libraries: Array<Library | PicoWirelessOption>;
134 codeOptions: CodeOption[];
137 toolchainVersion: string;
138 toolchainPath: string;
144 function getScriptsRoot(): string {
145 return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
148 export default class NewProjectCommand extends Command {
149 private readonly _logger: Logger = new Logger("NewProjectCommand");
150 private readonly _settings: Settings;
152 constructor(settings: Settings) {
155 this._settings = settings;
158 private async selectSDKAndToolchain(): Promise<
160 toolchainVersion: string;
161 toolchainPath: string;
167 const installedSDKs = detectInstalledSDKs();
168 const installedToolchains = detectInstalledToolchains();
171 if (installedSDKs.length === 0 || installedToolchains.length === 0) {
172 // TODO: add offline handling
173 const availableSDKs = await getSDKReleases();
174 const supportedToolchains = await getSupportedToolchains();
176 // show quick pick for sdk and toolchain
177 const selectedSDK = await window.showQuickPick(
178 availableSDKs.map(sdk => ({
179 label: `v${sdk.tagName}`,
183 placeHolder: "Select Pico SDK version",
184 title: "New Pico Project",
188 if (selectedSDK === undefined) {
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,
199 placeHolder: "Select ARM Embeded Toolchain version",
200 title: "New Pico Project",
204 if (selectedToolchain === undefined) {
208 // show user feedback as downloads can take a while
209 let installedSuccessfully = false;
210 await window.withProgress(
212 location: ProgressLocation.Notification,
213 title: "Downloading SDK and Toolchain",
219 !(await downloadAndInstallSDK(
220 selectedSDK.sdk.tagName,
223 !(await downloadAndInstallToolchain(selectedToolchain.toolchain))
225 Logger.log(`Failed to download and install toolchain and SDK.`);
229 "Failed to download and install toolchain and sdk. " +
230 "Make sure all requirements are met.",
234 installedSuccessfully = false;
236 installedSuccessfully = true;
241 if (!installedSuccessfully) {
245 toolchainVersion: selectedToolchain.toolchain.version,
246 toolchainPath: buildToolchainPath(
247 selectedToolchain.toolchain.version
249 sdkVersion: selectedSDK.sdk.tagName,
250 sdkPath: buildSDKPath(selectedSDK.sdk.tagName),
255 // sort installed sdks from newest to oldest
256 installedSDKs.sort((a, b) =>
257 compare(a.version.replace("v", ""), b.version.replace("v", ""))
259 // toolchains should be sorted and cant be sorted by compare because
260 // of their alphanumeric structure
263 toolchainVersion: installedToolchains[0].version,
264 toolchainPath: installedToolchains[0].path,
265 sdkVersion: installedSDKs[0].version,
266 sdkPath: installedSDKs[0].sdkPath,
270 `Error while retrieving SDK and toolchain versions: ${
271 error instanceof Error ? error.message : (error as string)
275 void window.showErrorMessage(
276 "Error while retrieving SDK and toolchain versions."
283 async execute(): Promise<void> {
284 // check if all requirements are met
285 const requirementsCheck = await checkForRequirements(this._settings);
286 if (!requirementsCheck[0]) {
287 void showRequirementsNotMetErrorMessage(requirementsCheck[1]);
292 // TODO: maybe make it posible to also select a folder and
293 // not always create a new one with selectedName
294 const projectRoot: Uri[] | undefined = await window.showOpenDialog({
295 canSelectFiles: false,
296 canSelectFolders: true,
297 canSelectMany: false,
298 openLabel: "Select project root",
301 // user focused out of the quick pick
302 if (!projectRoot || projectRoot.length !== 1) {
307 const selectedName: string | undefined = await window.showInputBox({
308 placeHolder: "Enter a project name",
309 title: "New Pico Project",
312 // user focused out of the quick pick
317 // get board type (single selection)
318 const selectedBoardType: BoardType | undefined =
319 (await window.showQuickPick(Object.values(BoardType), {
320 placeHolder: "Select a board type",
321 title: "New Pico Project",
322 })) as BoardType | undefined;
324 // user focused out of the quick pick
325 if (!selectedBoardType) {
329 // [optional] get console options (multi selection)
330 const selectedConsoleOptions: ConsoleOption[] | undefined =
331 (await window.showQuickPick(Object.values(ConsoleOption), {
332 placeHolder: "Would you like to enable the USB console?",
333 title: "New Pico Project",
335 })) as ConsoleOption[] | undefined;
337 // user focused out of the quick pick
338 if (!selectedConsoleOptions) {
342 // [optional] get libraries (multi selection)
343 const selectedFeatures: Array<Library | PicoWirelessOption> | undefined =
344 (await window.showQuickPick(Object.values(Library), {
345 placeHolder: "Select libraries to include",
346 title: "New Pico Project",
348 })) as Library[] | undefined;
350 // user focused out of the quick pick
351 if (!selectedFeatures) {
355 if (selectedBoardType === BoardType.picoW) {
356 const selectedWirelessFeature: PicoWirelessOption | undefined =
357 (await window.showQuickPick(Object.values(PicoWirelessOption), {
359 "Select wireless features to include or press enter to skip",
360 title: "New Pico Project",
362 })) as PicoWirelessOption | undefined;
364 // user focused out of the quick pick
365 if (!selectedWirelessFeature) {
370 selectedWirelessFeature &&
371 selectedWirelessFeature !== PicoWirelessOption.none
373 selectedFeatures.push(selectedWirelessFeature);
377 const selectedCodeOptions: CodeOption[] | undefined =
378 (await window.showQuickPick(Object.values(CodeOption), {
379 placeHolder: "Select code generator options to use",
380 title: "New Pico Project",
382 })) as CodeOption[] | undefined;
384 if (!selectedCodeOptions) {
388 const selectedDebugger: Debugger | undefined = (await window.showQuickPick(
389 Object.values(Debugger),
391 placeHolder: "Select debugger to use",
392 title: "New Pico Project",
395 )) as Debugger | undefined;
397 if (!selectedDebugger) {
401 const selectedToolchainAndSDK = await this.selectSDKAndToolchain();
403 if (selectedToolchainAndSDK === undefined) {
407 void window.showWarningMessage(
408 "Generating project, this may take a while..."
411 await this.executePicoProjectGenerator({
413 projectRoot: projectRoot[0].fsPath,
414 boardType: selectedBoardType,
415 consoleOptions: selectedConsoleOptions,
416 libraries: selectedFeatures,
417 codeOptions: selectedCodeOptions,
418 debugger: selectedDebugger,
419 toolchainAndSDK: selectedToolchainAndSDK,
423 private runGenerator(
426 ): Promise<number | null> {
427 return new Promise<number | null>(resolve => {
428 const generatorProcess = exec(command, options, error => {
430 console.error(`Error: ${error.message}`);
431 resolve(null); // Indicate error
435 generatorProcess.on("exit", code => {
436 // Resolve with exit code or -1 if code is undefined
443 * Executes the Pico Project Generator with the given options.
445 * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
447 private async executePicoProjectGenerator(
448 options: NewProjectOptions
450 const customEnv: { [key: string]: string } = {
451 ...(process.env as { [key: string]: string }),
453 ["PICO_SDK_PATH"]: options.toolchainAndSDK.sdkPath,
454 // set PICO_TOOLCHAIN_PATH i needed someday
455 ["PICO_TOOLCHAIN_PATH"]: options.toolchainAndSDK.toolchainPath,
457 // add compiler to PATH
458 const isWindows = process.platform === "win32";
459 customEnv[isWindows ? "Path" : "PATH"] = `${join(
460 options.toolchainAndSDK.toolchainPath,
462 )}${isWindows ? ";" : ":"}${customEnv[isWindows ? "Path" : "PATH"]}`;
464 this._settings.getString(SettingsKey.python3Path) || isWindows
468 const command: string = [
470 join(getScriptsRoot(), "pico_project.py"),
471 enumToParam(options.boardType),
472 ...options.consoleOptions.map(option => enumToParam(option)),
473 !options.consoleOptions.includes(ConsoleOption.consoleOverUART)
476 ...options.libraries.map(option => enumToParam(option)),
477 ...options.codeOptions.map(option => enumToParam(option)),
478 enumToParam(options.debugger),
479 // generate .vscode config
483 `"${options.projectRoot}"`,
485 options.toolchainAndSDK.sdkVersion,
486 "--toolchainVersion",
487 options.toolchainAndSDK.toolchainVersion,
491 this._logger.debug(`Executing project generator command: ${command}`);
494 // TODO: use exit codes to determine why the project generator failed (if it did)
495 // to be able to show the user a more detailed error message
496 const generatorExitCode = await this.runGenerator(command, {
498 cwd: getScriptsRoot(),
502 if (generatorExitCode === 0) {
503 void window.showInformationMessage(
504 `Successfully generated new project: ${options.name}`
508 void commands.executeCommand(
510 Uri.file(join(options.projectRoot, options.name)),
511 (workspace.workspaceFolders?.length ?? 0) > 0
515 `Generator Process exited with code: ${generatorExitCode ?? "null"}`
518 void window.showErrorMessage(
519 `Could not create new project: ${options.name}`