diff --git a/README.md b/README.md index 2cd23306..b242cbde 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,15 @@ For optimal functionality, consider enabling: When prompted, select the `Pico` kit in CMake Tools, and set your build and launch targets accordingly. Use CMake Tools for compilation, but continue using this extension for debugging, as CMake Tools debugging is not compatible with Pico. +## Rust Prerequisites + +* **rustup** – Installs and manages Rust. Get it from [rustup.rs](https://rustup.rs). +* A C compiler for your system: + + * **Linux**: `gcc` + * **macOS**: `clang` + * **Windows**: `MSVC` + ## VS Code Profiles If you work with multiple microcontroller toolchains, consider installing this extension into a [VS Code Profile](https://code.visualstudio.com/docs/editor/profiles) to avoid conflicts with other toolchains. Follow these steps: diff --git a/package-lock.json b/package-lock.json index 24865033..98c6f211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "got": "^14.4.7", "ini": "^5.0.0", "rimraf": "^6.0.1", + "toml": "^3.0.0", "undici": "^6.21.0", "uuid": "^11.1.0", "which": "^5.0.0" @@ -3816,6 +3817,12 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/package.json b/package.json index 1aa558be..95456e8c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ }, "activationEvents": [ "workspaceContains:./pico_sdk_import.cmake", + "workspaceContains:./.pico-rs", "onWebviewPanel:newPicoProject", "onWebviewPanel:newPicoMicroPythonProject" ], @@ -79,13 +80,13 @@ "command": "raspberry-pi-pico.switchSDK", "title": "Switch Pico SDK", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.switchBoard", "title": "Switch Board", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.launchTargetPath", @@ -93,6 +94,12 @@ "category": "Raspberry Pi Pico", "enablement": "false" }, + { + "command": "raspberry-pi-pico.launchTargetPathRelease", + "title": "Get path of the project release executable (rust only)", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, { "command": "raspberry-pi-pico.getPythonPath", "title": "Get python path", @@ -147,6 +154,18 @@ "category": "Raspberry Pi Pico", "enablement": "false" }, + { + "command": "raspberry-pi-pico.getOpenOCDRoot", + "title": "Get OpenOCD root", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, + { + "command": "raspberry-pi-pico.getSVDPath", + "title": "Get SVD Path (rust only)", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, { "command": "raspberry-pi-pico.compileProject", "title": "Compile Pico Project", @@ -185,13 +204,13 @@ "command": "raspberry-pi-pico.configureCmake", "title": "Configure CMake", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.switchBuildType", "title": "Switch Build Type", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.importProject", @@ -212,13 +231,19 @@ "command": "raspberry-pi-pico.flashProject", "title": "Flash Pico Project (SWD)", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.cleanCmake", "title": "Clean CMake", "category": "Raspberry Pi Pico", "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" + }, + { + "command": "raspberry-pi-pico.getRTTDecoderPath", + "title": "Get RTT Decoder module path", + "category": "Raspberry Pi Pico", + "enablement": "false" } ], "configuration": { @@ -326,6 +351,7 @@ "got": "^14.4.7", "ini": "^5.0.0", "rimraf": "^6.0.1", + "toml": "^3.0.0", "undici": "^6.21.0", "uuid": "^11.1.0", "which": "^5.0.0" diff --git a/src/commands/compileProject.mts b/src/commands/compileProject.mts index 7138472d..1f2e4e48 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -3,6 +3,7 @@ import { EventEmitter } from "events"; import { CommandWithResult } from "./command.mjs"; import Logger from "../logger.mjs"; import Settings, { SettingsKey } from "../settings.mjs"; +import State from "../state.mjs"; export default class CompileProjectCommand extends CommandWithResult { private _logger: Logger = new Logger("CompileProjectCommand"); @@ -18,9 +19,15 @@ export default class CompileProjectCommand extends CommandWithResult { const task = (await tasks.fetchTasks()).find( task => task.name === "Compile Project" ); + /*const isRustProject = await commands.executeCommand( + "getContext", + ContextKeys.isRustProject + );*/ + const isRustProject = State.getInstance().isRustProject; const settings = Settings.getInstance(); if ( + !isRustProject && settings !== undefined && settings.getBoolean(SettingsKey.useCmakeTools) ) { diff --git a/src/commands/conditionalDebugging.mts b/src/commands/conditionalDebugging.mts index d1148e8d..2643d87f 100644 --- a/src/commands/conditionalDebugging.mts +++ b/src/commands/conditionalDebugging.mts @@ -1,6 +1,8 @@ -import { Command } from "./command.mjs"; +import { Command, extensionName } from "./command.mjs"; import Logger from "../logger.mjs"; -import { commands } from "vscode"; +import { commands, window, workspace, debug } from "vscode"; +import State from "../state.mjs"; +import DebugLayoutCommand from "./debugLayout.mjs"; /** * Relay command for the default buildin debug select and start command. @@ -16,6 +18,23 @@ export default class ConditionalDebuggingCommand extends Command { } async execute(): Promise { + const isRustProject = State.getInstance().isRustProject; + + if (isRustProject) { + const wsFolder = workspace.workspaceFolders?.[0]; + if (!wsFolder) { + this._logger.error("No workspace folder found."); + void window.showErrorMessage("No workspace folder found."); + + return; + } + + void commands.executeCommand(`${extensionName}.${DebugLayoutCommand.id}`); + void debug.startDebugging(wsFolder, "Pico Debug (probe-rs)"); + + return; + } + await commands.executeCommand("workbench.action.debug.selectandstart"); } } diff --git a/src/commands/getPaths.mts b/src/commands/getPaths.mts index cfbc11d5..48e00c9e 100644 --- a/src/commands/getPaths.mts +++ b/src/commands/getPaths.mts @@ -1,5 +1,5 @@ import { CommandWithResult } from "./command.mjs"; -import { commands, workspace } from "vscode"; +import { commands, type Uri, window, workspace } from "vscode"; import { getPythonPath, getPath, @@ -7,15 +7,25 @@ import { cmakeGetPicoVar, } from "../utils/cmakeUtil.mjs"; import { join } from "path"; +import { join as joinPosix } from "path/posix"; import { + buildOpenOCDPath, buildPicotoolPath, + buildSDKPath, buildToolchainPath, + downloadAndInstallOpenOCD, downloadAndInstallPicotool, } from "../utils/download.mjs"; import Settings, { SettingsKey } from "../settings.mjs"; import which from "which"; import { execSync } from "child_process"; import { getPicotoolReleases } from "../utils/githubREST.mjs"; +import State from "../state.mjs"; +import VersionBundlesLoader from "../utils/versionBundles.mjs"; +import { getSupportedToolchains } from "../utils/toolchainUtil.mjs"; +import Logger from "../logger.mjs"; +import { rustProjectGetSelectedChip } from "../utils/rustUtil.mjs"; +import { OPENOCD_VERSION } from "../utils/sharedConstants.mjs"; export class GetPythonPathCommand extends CommandWithResult { constructor() { @@ -56,7 +66,7 @@ export class GetEnvPathCommand extends CommandWithResult { } export class GetGDBPathCommand extends CommandWithResult { - constructor() { + constructor(private readonly _extensionUri: Uri) { super("getGDBPath"); } @@ -69,13 +79,48 @@ export class GetGDBPathCommand extends CommandWithResult { } const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = State.getInstance().isRustProject; + let toolchainVersion = ""; - const selectedToolchainAndSDKVersions = - await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); - if (selectedToolchainAndSDKVersions === null) { - return ""; + if (isRustProject) { + // check if latest toolchain is installed + const vbl = new VersionBundlesLoader(this._extensionUri); + const latestVb = await vbl.getLatest(); + + if (!latestVb) { + void window.showErrorMessage("No version bundles found."); + + return ""; + } + + const supportedToolchains = await getSupportedToolchains(); + const latestSupportedToolchain = supportedToolchains.find( + t => t.version === latestVb.toolchain + ); + if (!latestSupportedToolchain) { + void window.showErrorMessage( + "No supported toolchain found for the latest version." + ); + + return ""; + } + + const useRISCV = rustProjectGetSelectedChip( + workspaceFolder.uri.fsPath + )?.includes("riscv"); + + toolchainVersion = useRISCV + ? latestVb.riscvToolchain + : latestVb.toolchain; + } else { + const selectedToolchainAndSDKVersions = + await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); + if (selectedToolchainAndSDKVersions === null) { + return ""; + } + + toolchainVersion = selectedToolchainAndSDKVersions[1]; } - const toolchainVersion = selectedToolchainAndSDKVersions[1]; let triple = "arm-none-eabi"; if (toolchainVersion.includes("RISCV")) { @@ -143,7 +188,8 @@ export class GetCompilerPathCommand extends CommandWithResult { } return join( - buildToolchainPath(toolchainVersion), "bin", + buildToolchainPath(toolchainVersion), + "bin", triple + `-gcc${process.platform === "win32" ? ".exe" : ""}` ); } @@ -181,15 +227,20 @@ export class GetCxxCompilerPathCommand extends CommandWithResult { } return join( - buildToolchainPath(toolchainVersion), "bin", + buildToolchainPath(toolchainVersion), + "bin", triple + `-g++${process.platform === "win32" ? ".exe" : ""}` ); } } export class GetChipCommand extends CommandWithResult { + private readonly _logger = new Logger("GetChipCommand"); + + public static readonly id = "getChip"; + constructor() { - super("getChip"); + super(GetChipCommand.id); } async execute(): Promise { @@ -201,6 +252,27 @@ export class GetChipCommand extends CommandWithResult { } const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = State.getInstance().isRustProject; + + if (isRustProject) { + // read .pico-rs + const chip = rustProjectGetSelectedChip(workspaceFolder.uri.fsPath); + if (chip === null) { + this._logger.error("Failed to read .pico-rs"); + + return ""; + } + + switch (chip) { + case "rp2040": + return "rp2040"; + case "rp2350": + case "rp2350-riscv": + return "rp235x"; + default: + return "rp2040"; + } + } const settings = Settings.getInstance(); let buildDir = join(workspaceFolder.uri.fsPath, "build"); @@ -261,6 +333,13 @@ export class GetTargetCommand extends CommandWithResult { } const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = State.getInstance().isRustProject; + + if (isRustProject) { + const chip = rustProjectGetSelectedChip(workspaceFolder.uri.fsPath); + + return chip === null ? "rp2040" : chip.toLowerCase(); + } const settings = Settings.getInstance(); let buildDir = join(workspaceFolder.uri.fsPath, "build"); @@ -301,8 +380,10 @@ export class GetPicotoolPathCommand extends CommandWithResult< > { private running: boolean = false; + public static readonly id = "getPicotoolPath"; + constructor() { - super("getPicotoolPath"); + super(GetPicotoolPathCommand.id); } async execute(): Promise { @@ -343,3 +424,82 @@ export class GetPicotoolPathCommand extends CommandWithResult< ); } } + +export class GetOpenOCDRootCommand extends CommandWithResult< + string | undefined +> { + private running: boolean = false; + + public static readonly id = "getOpenOCDRoot"; + + constructor() { + super(GetOpenOCDRootCommand.id); + } + + async execute(): Promise { + if (this.running) { + return undefined; + } + this.running = true; + + // check if it is installed if not install it + const result = await downloadAndInstallOpenOCD(OPENOCD_VERSION); + + if (result === null || !result) { + this.running = false; + + return undefined; + } + + this.running = false; + + return buildOpenOCDPath(OPENOCD_VERSION); + } +} + +/** + * Currently rust only! + */ +export class GetSVDPathCommand extends CommandWithResult { + public static readonly id = "getSVDPath"; + + constructor(private readonly _extensionUri: Uri) { + super(GetSVDPathCommand.id); + } + + async execute(): Promise { + if ( + workspace.workspaceFolders === undefined || + workspace.workspaceFolders.length === 0 + ) { + return ""; + } + + const isRustProject = State.getInstance().isRustProject; + if (!isRustProject) { + return; + } + + const vs = new VersionBundlesLoader(this._extensionUri); + const latestSDK = await vs.getLatestSDK(); + if (!latestSDK) { + return; + } + + const chip = rustProjectGetSelectedChip( + workspace.workspaceFolders[0].uri.fsPath + ); + + if (!chip) { + return; + } + + return joinPosix( + buildSDKPath(latestSDK), + "src", + chip, + "hardware_regs", + `${chip.toUpperCase()}.svd` + ); + } +} diff --git a/src/commands/launchTargetPath.mts b/src/commands/launchTargetPath.mts index 69951ba7..e92d2c4f 100644 --- a/src/commands/launchTargetPath.mts +++ b/src/commands/launchTargetPath.mts @@ -3,10 +3,16 @@ import { CommandWithResult } from "./command.mjs"; import { commands, window, workspace } from "vscode"; import { join } from "path"; import Settings, { SettingsKey } from "../settings.mjs"; +import State from "../state.mjs"; +import { parse as parseToml } from "toml"; +import { join as joinPosix } from "path/posix"; +import { rustProjectGetSelectedChip } from "../utils/rustUtil.mjs"; export default class LaunchTargetPathCommand extends CommandWithResult { + public static readonly id = "launchTargetPath"; + constructor() { - super("launchTargetPath"); + super(LaunchTargetPathCommand.id); } private async readProjectNameFromCMakeLists( @@ -61,6 +67,43 @@ export default class LaunchTargetPathCommand extends CommandWithResult { return ""; } + const isRustProject = State.getInstance().isRustProject; + + if (isRustProject) { + const cargoTomlPath = join( + workspace.workspaceFolders[0].uri.fsPath, + "Cargo.toml" + ); + const contents = readFileSync(cargoTomlPath, "utf-8"); + const cargoToml = (await parseToml(contents)) as + | { + package?: { name?: string }; + } + | undefined; + + if (cargoToml?.package?.name) { + const chip = rustProjectGetSelectedChip( + workspace.workspaceFolders[0].uri.fsPath + ); + const toolchain = + chip === "rp2040" + ? "thumbv6m-none-eabi" + : chip === "rp2350" + ? "thumbv8m.main-none-eabihf" + : "riscv32imac-unknown-none-elf"; + + return joinPosix( + workspace.workspaceFolders[0].uri.fsPath.replaceAll("\\", "/"), + "target", + toolchain, + "debug", + cargoToml.package.name + ); + } + + return ""; + } + const settings = Settings.getInstance(); if ( settings !== undefined && @@ -102,3 +145,59 @@ export default class LaunchTargetPathCommand extends CommandWithResult { ); } } + +export class LaunchTargetPathReleaseCommand extends CommandWithResult { + public static readonly id = "launchTargetPathRelease"; + + constructor() { + super(LaunchTargetPathReleaseCommand.id); + } + + async execute(): Promise { + if ( + workspace.workspaceFolders === undefined || + workspace.workspaceFolders.length === 0 + ) { + return ""; + } + + const isRustProject = State.getInstance().isRustProject; + + if (!isRustProject) { + return ""; + } + + const cargoTomlPath = join( + workspace.workspaceFolders[0].uri.fsPath, + "Cargo.toml" + ); + const contents = readFileSync(cargoTomlPath, "utf-8"); + const cargoToml = (await parseToml(contents)) as + | { + package?: { name?: string }; + } + | undefined; + + if (cargoToml?.package?.name) { + const chip = rustProjectGetSelectedChip( + workspace.workspaceFolders[0].uri.fsPath + ); + const toolchain = + chip === "rp2040" + ? "thumbv6m-none-eabi" + : chip === "rp2350" + ? "thumbv8m.main-none-eabihf" + : "riscv32imac-unknown-none-elf"; + + return joinPosix( + workspace.workspaceFolders[0].uri.fsPath.replaceAll("\\", "/"), + "target", + toolchain, + "release", + cargoToml.package.name + ); + } + + return ""; + } +} diff --git a/src/commands/newProject.mts b/src/commands/newProject.mts index 45c01a74..8e9f4cdc 100644 --- a/src/commands/newProject.mts +++ b/src/commands/newProject.mts @@ -4,6 +4,7 @@ import { window, type Uri } from "vscode"; import { NewProjectPanel } from "../webview/newProjectPanel.mjs"; // eslint-disable-next-line max-len import { NewMicroPythonProjectPanel } from "../webview/newMicroPythonProjectPanel.mjs"; +import { NewRustProjectPanel } from "../webview/newRustProjectPanel.mjs"; /** * Enum for the language of the project. @@ -13,6 +14,7 @@ import { NewMicroPythonProjectPanel } from "../webview/newMicroPythonProjectPane export enum ProjectLang { cCpp = 1, micropython = 2, + rust = 3, } export default class NewProjectCommand extends CommandWithArgs { @@ -20,6 +22,7 @@ export default class NewProjectCommand extends CommandWithArgs { private readonly _extensionUri: Uri; private static readonly micropythonOption = "MicroPython"; private static readonly cCppOption = "C/C++"; + private static readonly rustOption = "Rust (experimental)"; public static readonly id = "newProject"; @@ -34,6 +37,8 @@ export default class NewProjectCommand extends CommandWithArgs { ? NewProjectCommand.cCppOption : preSelectedType === ProjectLang.micropython ? NewProjectCommand.micropythonOption + : preSelectedType === ProjectLang.rust + ? NewProjectCommand.rustOption : undefined; } @@ -42,7 +47,11 @@ export default class NewProjectCommand extends CommandWithArgs { const lang = this.preSelectedTypeToStr(preSelectedType) ?? (await window.showQuickPick( - [NewProjectCommand.cCppOption, NewProjectCommand.micropythonOption], + [ + NewProjectCommand.cCppOption, + NewProjectCommand.micropythonOption, + NewProjectCommand.rustOption, + ], { placeHolder: "Select which language to use for your new project", canPickMany: false, @@ -58,6 +67,9 @@ export default class NewProjectCommand extends CommandWithArgs { if (lang === NewProjectCommand.micropythonOption) { // create a new project with MicroPython NewMicroPythonProjectPanel.createOrShow(this._extensionUri); + } else if (lang === NewProjectCommand.rustOption) { + // create a new project with Rust + NewRustProjectPanel.createOrShow(this._extensionUri); } else { // show webview where the process of creating a new project is continued NewProjectPanel.createOrShow(this._extensionUri); diff --git a/src/commands/switchBoard.mts b/src/commands/switchBoard.mts index 71e14890..5babad9e 100644 --- a/src/commands/switchBoard.mts +++ b/src/commands/switchBoard.mts @@ -1,11 +1,16 @@ import { Command } from "./command.mjs"; import Logger from "../logger.mjs"; import { - commands, ProgressLocation, window, workspace, type Uri + commands, + ProgressLocation, + window, + workspace, + type Uri, } from "vscode"; -import { existsSync, readdirSync, readFileSync } from "fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs"; import { - buildSDKPath, downloadAndInstallToolchain + buildSDKPath, + downloadAndInstallToolchain, } from "../utils/download.mjs"; import { cmakeGetSelectedToolchainAndSDKVersions, @@ -21,8 +26,11 @@ import type UI from "../ui.mjs"; import { updateVSCodeStaticConfigs } from "../utils/vscodeConfigUtil.mjs"; import { getSupportedToolchains } from "../utils/toolchainUtil.mjs"; import VersionBundlesLoader from "../utils/versionBundles.mjs"; +import State from "../state.mjs"; +import { unknownErrorToString } from "../utils/errorHelper.mjs"; export default class SwitchBoardCommand extends Command { + private _logger: Logger = new Logger("SwitchBoardCommand"); private _versionBundlesLoader: VersionBundlesLoader; public static readonly id = "switchBoard"; @@ -32,8 +40,9 @@ export default class SwitchBoardCommand extends Command { this._versionBundlesLoader = new VersionBundlesLoader(extensionUri); } - public static async askBoard(sdkVersion: string): - Promise<[string, boolean] | undefined> { + public static async askBoard( + sdkVersion: string + ): Promise<[string, boolean] | undefined> { const quickPickItems: string[] = ["pico", "pico_w"]; const workspaceFolder = workspace.workspaceFolders?.[0]; @@ -112,7 +121,6 @@ export default class SwitchBoardCommand extends Command { }); if (board === undefined) { - return board; } @@ -120,7 +128,6 @@ export default class SwitchBoardCommand extends Command { const data = readFileSync(boardFiles[board]) if (data.includes("rp2040")) { - return [board, false]; } @@ -129,7 +136,6 @@ export default class SwitchBoardCommand extends Command { }); if (useRiscV === undefined) { - return undefined; } @@ -138,15 +144,66 @@ export default class SwitchBoardCommand extends Command { async execute(): Promise { const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = State.getInstance().isRustProject; // check it has a CMakeLists.txt if ( workspaceFolder === undefined || - !existsSync(join(workspaceFolder.uri.fsPath, "CMakeLists.txt")) + (!existsSync(join(workspaceFolder.uri.fsPath, "CMakeLists.txt")) && + !isRustProject) ) { return; } + if (isRustProject) { + const board = await window.showQuickPick( + ["RP2040", "RP2350", "RP2350-RISCV"], + { + placeHolder: "Select chip", + canPickMany: false, + ignoreFocusOut: false, + title: "Switch project target chip", + } + ); + + if (board === undefined) { + return undefined; + } + + try { + writeFileSync( + join(workspaceFolder.uri.fsPath, ".pico-rs"), + board.toLowerCase(), + "utf8" + ); + } catch (error) { + this._logger.error( + `Failed to write .pico-rs file: ${unknownErrorToString(error)}` + ); + + void window.showErrorMessage( + "Failed to write .pico-rs file. " + + "Please check the logs for more information." + ); + + return; + } + + this._ui.updateBoard(board.toUpperCase()); + const toolchain = + board === "RP2040" + ? "thumbv6m-none-eabi" + : board === "RP2350" + ? "thumbv8m.main-none-eabihf" + : "riscv32imac-unknown-none-elf"; + + await workspace + .getConfiguration("rust-analyzer") + .update("cargo.target", toolchain, null); + + return; + } + const versions = await cmakeGetSelectedToolchainAndSDKVersions( workspaceFolder.uri ); @@ -205,22 +262,19 @@ export default class SwitchBoardCommand extends Command { const selectedToolchain = supportedToolchainVersions.find( t => t.version === chosenToolchainVersion - ) + ); if (selectedToolchain === undefined) { - void window.showErrorMessage( - "Error switching to Risc-V toolchain" - ); + void window.showErrorMessage("Error switching to Risc-V toolchain"); return; } await window.withProgress( - { - title: - `Installing toolchain ${selectedToolchain.version} `, - location: ProgressLocation.Notification, - }, + { + title: `Installing toolchain ${selectedToolchain.version} `, + location: ProgressLocation.Notification, + }, async progress => { if (await downloadAndInstallToolchain(selectedToolchain)) { progress.report({ @@ -253,7 +307,7 @@ export default class SwitchBoardCommand extends Command { } } } - ) + ); } const success = await cmakeUpdateBoard(workspaceFolder.uri, board); diff --git a/src/contextKeys.mts b/src/contextKeys.mts index c1f79c13..4b515be4 100644 --- a/src/contextKeys.mts +++ b/src/contextKeys.mts @@ -2,4 +2,5 @@ import { extensionName } from "./commands/command.mjs"; export enum ContextKeys { isPicoProject = `${extensionName}.isPicoProject`, + isRustProject = `${extensionName}.isRustProject`, } diff --git a/src/extension.mts b/src/extension.mts index 20372ceb..ec19cf77 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -33,7 +33,9 @@ import { existsSync, readFileSync } from "fs"; import { basename, join } from "path"; import CompileProjectCommand from "./commands/compileProject.mjs"; import RunProjectCommand from "./commands/runProject.mjs"; -import LaunchTargetPathCommand from "./commands/launchTargetPath.mjs"; +import LaunchTargetPathCommand, { + LaunchTargetPathReleaseCommand, +} from "./commands/launchTargetPath.mjs"; import { GetPythonPathCommand, GetEnvPathCommand, @@ -44,6 +46,8 @@ import { GetTargetCommand, GetChipUppercaseCommand, GetPicotoolPathCommand, + GetOpenOCDRootCommand, + GetSVDPathCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -53,13 +57,13 @@ import { downloadAndInstallTools, downloadAndInstallPicotool, downloadAndInstallOpenOCD, + installLatestRustRequirements, } from "./utils/download.mjs"; import { SDK_REPOSITORY_URL } from "./utils/githubREST.mjs"; import { getSupportedToolchains } from "./utils/toolchainUtil.mjs"; import { NewProjectPanel, getWebviewOptions, - openOCDVersion, } from "./webview/newProjectPanel.mjs"; import GithubApiCache from "./utils/githubApiCache.mjs"; import ClearGithubApiCacheCommand from "./commands/clearGithubApiCache.mjs"; @@ -82,6 +86,13 @@ import FlashProjectSWDCommand from "./commands/flashProjectSwd.mjs"; import { NewMicroPythonProjectPanel } from "./webview/newMicroPythonProjectPanel.mjs"; import type { Progress as GotProgress } from "got"; import findPython, { showPythonNotFoundError } from "./utils/pythonHelper.mjs"; +import { + downloadAndInstallRust, + rustProjectGetSelectedChip, +} from "./utils/rustUtil.mjs"; +import State from "./state.mjs"; +import { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; +import { OPENOCD_VERSION } from "./utils/sharedConstants.mjs"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -108,15 +119,18 @@ export async function activate(context: ExtensionContext): Promise { new SwitchSDKCommand(ui, context.extensionUri), new SwitchBoardCommand(ui, context.extensionUri), new LaunchTargetPathCommand(), + new LaunchTargetPathReleaseCommand(), new GetPythonPathCommand(), new GetEnvPathCommand(), - new GetGDBPathCommand(), + new GetGDBPathCommand(context.extensionUri), new GetCompilerPathCommand(), new GetCxxCompilerPathCommand(), new GetChipCommand(), new GetChipUppercaseCommand(), new GetTargetCommand(), new GetPicotoolPathCommand(), + new GetOpenOCDRootCommand(), + new GetSVDPathCommand(context.extensionUri), new CompileProjectCommand(), new RunProjectCommand(), new FlashProjectSWDCommand(), @@ -167,6 +181,17 @@ export async function activate(context: ExtensionContext): Promise { }) ); + context.subscriptions.push( + window.registerWebviewPanelSerializer(NewRustProjectPanel.viewType, { + // eslint-disable-next-line @typescript-eslint/require-await + async deserializeWebviewPanel(webviewPanel: WebviewPanel): Promise { + // Reset the webview options so we use latest uri for `localResourceRoots`. + webviewPanel.webview.options = getWebviewOptions(context.extensionUri); + NewRustProjectPanel.revive(webviewPanel, context.extensionUri); + }, + }) + ); + context.subscriptions.push( window.registerTreeDataProvider( PicoProjectActivityBar.viewType, @@ -175,6 +200,9 @@ export async function activate(context: ExtensionContext): Promise { ); const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = workspaceFolder + ? existsSync(join(workspaceFolder.uri.fsPath, ".pico-rs")) + : false; // check if there is a workspace folder if (workspaceFolder === undefined) { @@ -192,20 +220,31 @@ export async function activate(context: ExtensionContext): Promise { return; } - const cmakeListsFilePath = join(workspaceFolder.uri.fsPath, "CMakeLists.txt"); - if (!existsSync(cmakeListsFilePath)) { - Logger.warn( - LoggerSource.extension, - "No CMakeLists.txt in workspace folder has been found." - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false + void commands.executeCommand( + "setContext", + ContextKeys.isRustProject, + isRustProject + ); + State.getInstance().isRustProject = isRustProject; + + if (!isRustProject) { + const cmakeListsFilePath = join( + workspaceFolder.uri.fsPath, + "CMakeLists.txt" ); + if (!existsSync(cmakeListsFilePath)) { + Logger.warn( + LoggerSource.extension, + "No CMakeLists.txt in workspace folder has been found." + ); + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + false + ); - return; - } + return; + } // check for pico_sdk_init() in CMakeLists.txt if ( @@ -226,45 +265,79 @@ export async function activate(context: ExtensionContext): Promise { return; } - // check if it has .vscode folder and cmake donotedit header in CMakelists.txt - if ( - !existsSync(join(workspaceFolder.uri.fsPath, ".vscode")) || - !( - readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX) || - readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX_OLD) - ) - ) { - Logger.warn( - LoggerSource.extension, - "No .vscode folder and/or cmake", - '"DO NOT EDIT"-header in CMakelists.txt found.' - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); - const wantToImport = await window.showInformationMessage( - "Do you want to import this project as Raspberry Pi Pico project?", - "Yes", - "No" - ); - if (wantToImport === "Yes") { - void commands.executeCommand( - `${extensionName}.${ImportProjectCommand.id}`, - workspaceFolder.uri + // check if it has .vscode folder and cmake donotedit header in CMakelists.txt + if ( + !existsSync(join(workspaceFolder.uri.fsPath, ".vscode")) || + !( + readFileSync(cmakeListsFilePath) + .toString("utf-8") + .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX) || + readFileSync(cmakeListsFilePath) + .toString("utf-8") + .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX_OLD) + ) + ) { + Logger.warn( + LoggerSource.extension, + "No .vscode folder and/or cmake", + '"DO NOT EDIT"-header in CMakelists.txt found.' ); - } + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + false + ); + const wantToImport = await window.showInformationMessage( + "Do you want to import this project as Raspberry Pi Pico project?", + "Yes", + "No" + ); + if (wantToImport === "Yes") { + void commands.executeCommand( + `${extensionName}.${ImportProjectCommand.id}`, + workspaceFolder.uri + ); + } - return; + return; + } } await commands.executeCommand("setContext", ContextKeys.isPicoProject, true); + if (isRustProject) { + const cargo = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Rust. This may take a while...", + cancellable: false, + }, + async () => downloadAndInstallRust() + ); + if (!cargo) { + void window.showErrorMessage("Failed to install Rust."); + + return; + } + + const result = await installLatestRustRequirements(context.extensionUri); + + if (!result) { + return; + } + + ui.showStatusBarItems(isRustProject); + + const chip = rustProjectGetSelectedChip(workspaceFolder.uri.fsPath); + if (chip !== null) { + ui.updateBoard(chip.toUpperCase()); + } else { + ui.updateBoard("N/A"); + } + + return; + } + // get sdk selected in the project const selectedToolchainAndSDKVersions = await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); @@ -493,7 +566,7 @@ export async function activate(context: ExtensionContext): Promise { }, async progress => { const result = await downloadAndInstallOpenOCD( - openOCDVersion, + OPENOCD_VERSION, (prog: GotProgress) => { const percent = prog.percent * 100; progress.report({ @@ -794,7 +867,7 @@ export async function activate(context: ExtensionContext): Promise { await configureCmakeNinja(workspaceFolder.uri); const ws = workspaceFolder.uri.fsPath; - const cMakeCachePath = join(ws, "build","CMakeCache.txt"); + const cMakeCachePath = join(ws, "build", "CMakeCache.txt"); const newBuildType = cmakeGetPicoVar(cMakeCachePath, "CMAKE_BUILD_TYPE"); ui.updateBuildType(newBuildType ?? "unknown"); diff --git a/src/logger.mts b/src/logger.mts index a836b7c0..c88860f2 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -42,6 +42,8 @@ export enum LoggerSource { pythonHelper = "pythonHelper", gitUtil = "gitUtil", vscodeConfigUtil = "vscodeConfigUtil", + rustUtil = "rustUtil", + projectRust = "projectRust", } /** diff --git a/src/state.mts b/src/state.mts new file mode 100644 index 00000000..054c97ce --- /dev/null +++ b/src/state.mts @@ -0,0 +1,14 @@ +export default class State { + private static instance?: State; + public isRustProject = false; + + public constructor() {} + + public static getInstance(): State { + if (!State.instance) { + this.instance = new State(); + } + + return this.instance!; + } +} diff --git a/src/ui.mts b/src/ui.mts index 5a8769fa..921d67be 100644 --- a/src/ui.mts +++ b/src/ui.mts @@ -1,6 +1,7 @@ import { window, type StatusBarItem, StatusBarAlignment } from "vscode"; import Logger from "./logger.mjs"; import type { PicoProjectActivityBar } from "./webview/activityBar.mjs"; +import State from "./state.mjs"; enum StatusBarItemKey { compile = "raspberry-pi-pico.compileProject", @@ -10,29 +11,40 @@ enum StatusBarItemKey { } const STATUS_BAR_ITEMS: { - [key: string]: { text: string; command: string; tooltip: string }; + [key: string]: { + text: string; + rustText?: string; + command: string; + tooltip: string; + rustSupport: boolean; + }; } = { [StatusBarItemKey.compile]: { // alt. "$(gear) Compile" text: "$(file-binary) Compile", command: "raspberry-pi-pico.compileProject", tooltip: "Compile Project", + rustSupport: true, }, [StatusBarItemKey.run]: { // alt. "$(gear) Compile" text: "$(run) Run", command: "raspberry-pi-pico.runProject", tooltip: "Run Project", + rustSupport: true, }, [StatusBarItemKey.picoSDKQuickPick]: { text: "Pico SDK: ", command: "raspberry-pi-pico.switchSDK", tooltip: "Select Pico SDK", + rustSupport: false, }, [StatusBarItemKey.picoBoardQuickPick]: { text: "Board: ", + rustText: "Chip: ", command: "raspberry-pi-pico.switchBoard", - tooltip: "Select Board", + tooltip: "Select Chip", + rustSupport: true, }, }; @@ -57,8 +69,10 @@ export default class UI { }); } - public showStatusBarItems(): void { - Object.values(this._items).forEach(item => item.show()); + public showStatusBarItems(isRustProject = false): void { + Object.values(this._items) + .filter(item => !isRustProject || STATUS_BAR_ITEMS[item.id].rustSupport) + .forEach(item => item.show()); } public updateSDKVersion(version: string): void { @@ -69,10 +83,20 @@ export default class UI { } public updateBoard(board: string): void { - this._items[StatusBarItemKey.picoBoardQuickPick].text = STATUS_BAR_ITEMS[ - StatusBarItemKey.picoBoardQuickPick - ].text.replace("", board); - this._activityBarProvider.refreshBoard(board); + const isRustProject = State.getInstance().isRustProject; + + if ( + isRustProject && + STATUS_BAR_ITEMS[StatusBarItemKey.picoBoardQuickPick].rustSupport + ) { + this._items[StatusBarItemKey.picoBoardQuickPick].text = STATUS_BAR_ITEMS[ + StatusBarItemKey.picoBoardQuickPick + ].rustText!.replace("", board); + } else { + this._items[StatusBarItemKey.picoBoardQuickPick].text = STATUS_BAR_ITEMS[ + StatusBarItemKey.picoBoardQuickPick + ].text.replace("", board); + } } public updateBuildType(buildType: string): void { diff --git a/src/utils/download.mts b/src/utils/download.mts index c888ad50..9c7db547 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -14,12 +14,15 @@ import Logger, { LoggerSource } from "../logger.mjs"; import { STATUS_CODES } from "http"; import type { Dispatcher } from "undici"; import { Client } from "undici"; -import type { SupportedToolchainVersion } from "./toolchainUtil.mjs"; +import { + getSupportedToolchains, + type SupportedToolchainVersion, +} from "./toolchainUtil.mjs"; import { cloneRepository, initSubmodules, ensureGit } from "./gitUtil.mjs"; import { HOME_VAR, SettingsKey } from "../settings.mjs"; import Settings from "../settings.mjs"; import which from "which"; -import { window } from "vscode"; +import { ProgressLocation, type Uri, window } from "vscode"; import { fileURLToPath } from "url"; import { type GithubReleaseAssetData, @@ -34,6 +37,7 @@ import { HTTP_STATUS_UNAUTHORIZED, githubApiUnauthorized, HTTP_STATUS_FORBIDDEN, + HTTP_STATUS_OK, } from "./githubREST.mjs"; import { unxzFile, unzipFile } from "./downloadHelpers.mjs"; import type { Writable } from "stream"; @@ -42,10 +46,12 @@ import { got, type Progress } from "got"; import { pipeline as streamPipeline } from "node:stream/promises"; import { CURRENT_PYTHON_VERSION, + OPENOCD_VERSION, WINDOWS_ARM64_PYTHON_DOWNLOAD_URL, WINDOWS_X86_PYTHON_DOWNLOAD_URL, } from "./sharedConstants.mjs"; import { compareGe } from "./semverUtil.mjs"; +import VersionBundlesLoader from "./versionBundles.mjs"; /// Translate nodejs platform names to ninja platform names const NINJA_PLATFORMS: { [key: string]: string } = { @@ -186,6 +192,30 @@ export function buildPython3Path(version: string): string { ); } +export async function downloadAndReadFile( + url: string +): Promise { + const response = await got(url, { + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "User-Agent": EXT_USER_AGENT, + // eslint-disable-next-line @typescript-eslint/naming-convention + Accept: "*/*", + // eslint-disable-next-line @typescript-eslint/naming-convention + "Accept-Encoding": "gzip, deflate, br", + }, + followRedirect: true, + method: "GET", + retry: { + limit: 3, + methods: ["GET"], + }, + cache: false, + }); + + return response.statusCode === HTTP_STATUS_OK ? response.body : undefined; +} + /** * Downloads and installs an archive from a URL. * @@ -211,7 +241,8 @@ export async function downloadAndInstallArchive( extraCallback?: () => void, redirectURL?: string, extraHeaders?: { [key: string]: string }, - progressCallback?: (progress: Progress) => void + progressCallback?: (progress: Progress) => void, + xzSingleDirOption?: string ): Promise { // Check if already installed if ( @@ -230,7 +261,7 @@ export async function downloadAndInstallArchive( if (!archiveExtension) { Logger.error( LoggerSource.downloader, - `Could not determine archive extension for ${url}` + `Could not determine archive extension for ${archiveFileName}` ); return false; @@ -281,7 +312,8 @@ export async function downloadAndInstallArchive( const unpackResult = await unpackArchive( archiveFilePath, targetDirectory, - archiveExtension + archiveExtension, + xzSingleDirOption ); if (unpackResult && extraCallback) { @@ -406,11 +438,16 @@ async function downloadFileGot( async function unpackArchive( archiveFilePath: string, targetDirectory: string, - archiveExt: string + archiveExt: string, + xzSingleDirOption?: string ): Promise { try { if (archiveExt === "tar.xz" || archiveExt === "tar.gz") { - const success = await unxzFile(archiveFilePath, targetDirectory); + const success = await unxzFile( + archiveFilePath, + targetDirectory, + xzSingleDirOption + ); cleanupFiles(archiveFilePath); return success; @@ -571,8 +608,8 @@ export async function downloadAndInstallSDK( * @param redirectURL An optional redirect URL to download the asset * from (used to follow redirects recursively) * @returns A promise that resolves to true if the asset was downloaded and installed successfully - */ -async function downloadAndInstallGithubAsset( + */ // TODO: do not export +export async function downloadAndInstallGithubAsset( version: string, releaseVersion: string, repo: GithubRepository, @@ -1270,3 +1307,152 @@ export async function downloadEmbedPython( return; } } + +/** + * Downloads and installs the latest SDK and toolchains. + * + * + OpenOCD + picotool + * (includes UI feedback) + * + * @param extensionUri The URI of the extension + */ +export async function installLatestRustRequirements( + extensionUri: Uri +): Promise { + const vb = new VersionBundlesLoader(extensionUri); + const latest = await vb.getLatest(); + if (latest === undefined) { + void window.showErrorMessage( + "Failed to get latest version bundles. " + + "Please try again and check your settings." + ); + + return false; + } + + const supportedToolchains = await getSupportedToolchains(); + + let result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Downloading ARM Toolchain for debugging...`, + }, + async progress => { + const toolchain = supportedToolchains.find( + t => t.version === latest.toolchain + ); + + if (toolchain === undefined) { + void window.showErrorMessage( + "Failed to get default toolchain. " + + "Please try again and check your internet connection." + ); + + return false; + } + + let progressState = 0; + + return downloadAndInstallToolchain(toolchain, (prog: Progress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }); + } + ); + + if (!result) { + void window.showErrorMessage( + "Failed to download ARM Toolchain. " + + "Please try again and check your settings." + ); + + return false; + } + + result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading RISC-V Toolchain for debugging...", + }, + async progress => { + const toolchain = supportedToolchains.find( + t => t.version === latest.riscvToolchain + ); + + if (toolchain === undefined) { + void window.showErrorMessage( + "Failed to get default RISC-V toolchain. " + + "Please try again and check your internet connection." + ); + + return false; + } + + let progressState = 0; + + return downloadAndInstallToolchain(toolchain, (prog: Progress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }); + } + ); + + if (!result) { + void window.showErrorMessage( + "Failed to download RISC-V Toolchain. " + + "Please try again and check your internet connection." + ); + + return false; + } + + result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing OpenOCD...", + }, + async progress => { + let progressState = 0; + + return downloadAndInstallOpenOCD(OPENOCD_VERSION, (prog: Progress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }); + } + ); + if (!result) { + void window.showErrorMessage( + "Failed to download OpenOCD. " + + "Please try again and check your internet connection." + ); + + return false; + } + + result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing picotool...", + }, + async progress => { + let progressState = 0; + + return downloadAndInstallPicotool(latest.picotool, (prog: Progress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }); + } + ); + if (!result) { + void window.showErrorMessage( + "Failed to download picotool. " + + "Please try again and check your internet connection." + ); + } + + return result; +} diff --git a/src/utils/downloadHelpers.mts b/src/utils/downloadHelpers.mts index ce370cc0..43f965ed 100644 --- a/src/utils/downloadHelpers.mts +++ b/src/utils/downloadHelpers.mts @@ -96,7 +96,7 @@ export function unzipFile( * * Also supports tar.gz files. * - * Linux and macOS only. + * Linux, macOS and Windows >= 10.0.17063.0. * * @param xzFilePath * @param targetDirectory @@ -104,19 +104,29 @@ export function unzipFile( */ export async function unxzFile( xzFilePath: string, - targetDirectory: string + targetDirectory: string, + singleDir?: string ): Promise { - if (process.platform === "win32") { - return false; - } - return new Promise(resolve => { try { - // Construct the command to extract the .xz file using the 'tar' command - // -J option is redundant in modern versions of tar, but it's still good for compatibility - const command = `tar -x${ - xzFilePath.endsWith(".xz") ? "J" : "z" - }f "${xzFilePath}" -C "${targetDirectory}"`; + let command = ""; + + if (process.platform === "win32") { + // Construct the command to extract the .xz file using the 'tar' command + command = `tar -xf "${xzFilePath}" -C "${targetDirectory}"`; + if (singleDir) { + command += ` "${singleDir}"`; + } + } else { + // Construct the command to extract the .xz file using the 'tar' command + // -J option is redundant in modern versions of tar, but it's still good for compatibility + command = `tar -x${ + xzFilePath.endsWith(".xz") ? "J" : "z" + }f "${xzFilePath}" -C "${targetDirectory}"`; + if (singleDir) { + command += ` --strip-components=1 '${singleDir}'`; + } + } // Execute the 'tar' command in the shell exec(command, error => { @@ -128,27 +138,34 @@ export async function unxzFile( ); resolve(false); } else { - const targetDirContents = readdirSync(targetDirectory); - const subfolderPath = - targetDirContents.length === 1 - ? join(targetDirectory, targetDirContents[0]) - : ""; - if ( - targetDirContents.length === 1 && - statSync(subfolderPath).isDirectory() - ) { - // Move all files and folders from the subfolder to targetDirectory - readdirSync(subfolderPath).forEach(item => { - const itemPath = join(subfolderPath, item); - const newItemPath = join(targetDirectory, item); - - // Use fs.renameSync to move the item - renameSync(itemPath, newItemPath); - }); - - // Remove the empty subfolder - rmdirSync(subfolderPath); - } + // flatten structure + let targetDirContents = readdirSync(targetDirectory); + do { + const subfolderPath = + targetDirContents.length === 1 + ? join(targetDirectory, targetDirContents[0]) + : ""; + if ( + targetDirContents.length === 1 && + statSync(subfolderPath).isDirectory() + ) { + // Move all files and folders from the subfolder to targetDirectory + readdirSync(subfolderPath).forEach(item => { + const itemPath = join(subfolderPath, item); + const newItemPath = join(targetDirectory, item); + + // Use fs.renameSync to move the item + renameSync(itemPath, newItemPath); + }); + + // Remove the empty subfolder + rmdirSync(subfolderPath); + } + if (!singleDir) { + break; + } + targetDirContents = readdirSync(targetDirectory); + } while (targetDirContents.length === 1); Logger.debug( LoggerSource.downloadHelper, diff --git a/src/utils/githubREST.mts b/src/utils/githubREST.mts index 0883564e..2bdfe45d 100644 --- a/src/utils/githubREST.mts +++ b/src/utils/githubREST.mts @@ -25,6 +25,8 @@ export enum GithubRepository { ninja = 2, tools = 3, picotool = 4, + rust = 5, + rsTools = 6, } /** @@ -68,6 +70,10 @@ export function ownerOfRepository(repository: GithubRepository): string { return "Kitware"; case GithubRepository.ninja: return "ninja-build"; + case GithubRepository.rust: + return "rust-lang"; + case GithubRepository.rsTools: + return "paulober"; } } @@ -90,6 +96,10 @@ export function repoNameOfRepository(repository: GithubRepository): string { return "pico-sdk-tools"; case GithubRepository.picotool: return "picotool"; + case GithubRepository.rust: + return "rust"; + case GithubRepository.rsTools: + return "pico-vscode-rs-tools"; } } @@ -307,6 +317,14 @@ export async function getCmakeReleases(): Promise { return getReleases(GithubRepository.cmake); } +export async function getRustReleases(): Promise { + return getReleases(GithubRepository.rust); +} + +export async function getRustToolsReleases(): Promise { + return getReleases(GithubRepository.rsTools); +} + /** * Get the release data for a specific tag from * the GitHub RESY API. diff --git a/src/utils/projectGeneration/projectRust.mts b/src/utils/projectGeneration/projectRust.mts new file mode 100644 index 00000000..c9547ba1 --- /dev/null +++ b/src/utils/projectGeneration/projectRust.mts @@ -0,0 +1,1395 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { join } from "path"; +import { TomlInlineObject, writeTomlFile } from "./tomlUtil.mjs"; +import Logger, { LoggerSource } from "../../logger.mjs"; +import { unknownErrorToString } from "../errorHelper.mjs"; +import { mkdir, writeFile } from "fs/promises"; +import { + GetChipCommand, + GetOpenOCDRootCommand, + GetPicotoolPathCommand, + GetSVDPathCommand, +} from "../../commands/getPaths.mjs"; +import { extensionName } from "../../commands/command.mjs"; +import { commands, window } from "vscode"; +import LaunchTargetPathCommand from "../../commands/launchTargetPath.mjs"; + +async function generateVSCodeConfig(projectRoot: string): Promise { + const vsc = join(projectRoot, ".vscode"); + + // create extensions.json + const extensions = { + recommendations: [ + "marus25.cortex-debug", + "rust-lang.rust-analyzer", + "probe-rs.probe-rs-debugger", + "raspberry-pi.raspberry-pi-pico", + ], + }; + + const openOCDPath: string | undefined = await commands.executeCommand( + `${extensionName}.${GetOpenOCDRootCommand.id}` + ); + if (!openOCDPath) { + Logger.error(LoggerSource.projectRust, "Failed to get OpenOCD path"); + + void window.showErrorMessage("Failed to get OpenOCD path"); + + return false; + } + + // TODO: get commands dynamically + const launch = { + version: "0.2.0", + configurations: [ + { + name: "Pico Debug (probe-rs)", + cwd: "${workspaceFolder}", + request: "launch", + type: "probe-rs-debug", + connectUnderReset: false, + speed: 5000, + runtimeExecutable: "probe-rs", + chip: `\${command:${extensionName}.${GetChipCommand.id}}`, + runtimeArgs: ["dap-server"], + flashingConfig: { + flashingEnabled: true, + haltAfterReset: false, + }, + coreConfigs: [ + { + coreIndex: 0, + programBinary: `\${command:${extensionName}.${LaunchTargetPathCommand.id}}`, + rttEnabled: true, + svdFile: `\${command:${extensionName}.${GetSVDPathCommand.id}}`, + rttChannelFormats: [ + { + channelNumber: 0, + dataFormat: "Defmt", + mode: "NoBlockSkip", + showTimestamps: true, + }, + ], + }, + ], + preLaunchTask: "Compile Project (debug)", + consoleLogLevel: "Debug", + wireProtocol: "Swd", + }, + ], + }; + + const settings = { + "rust-analyzer.cargo.target": "thumbv8m.main-none-eabihf", + "rust-analyzer.checkOnSave.allTargets": false, + "editor.formatOnSave": true, + "files.exclude": { + ".pico-rs": true, + }, + }; + + const tasks = { + version: "2.0.0", + tasks: [ + { + label: "Compile Project", + type: "process", + isBuildCommand: true, + command: "cargo", + args: ["build", "--release"], + group: { + kind: "build", + isDefault: true, + }, + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: "$rustc", + options: { + env: { + PICOTOOL_PATH: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + CHIP: `\${command:${extensionName}.${GetChipCommand.id}}`, + }, + }, + }, + { + label: "Compile Project (debug)", + type: "process", + isBuildCommand: true, + command: "cargo", + args: ["build"], + group: { + kind: "build", + isDefault: false, + }, + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: "$rustc", + options: { + env: { + PICOTOOL_PATH: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + CHIP: `\${command:${extensionName}.${GetChipCommand.id}}`, + }, + }, + }, + { + label: "Run Project", + type: "shell", + dependsOn: "Compile Project", + command: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + args: [ + "load", + "-x", + "${command:raspberry-pi-pico.launchTargetPathRelease}", + "-t", + "elf", + ], + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: [], + }, + ], + }; + + try { + await mkdir(vsc, { recursive: true }); + await writeFile(join(vsc, "extensions.json"), JSON.stringify(extensions)); + await writeFile(join(vsc, "launch.json"), JSON.stringify(launch, null, 2)); + await writeFile( + join(vsc, "settings.json"), + JSON.stringify(settings, null, 2) + ); + await writeFile(join(vsc, "tasks.json"), JSON.stringify(tasks, null, 2)); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write extensions.json file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateMainRs(projectRoot: string): Promise { + const mainRs = `//! # GPIO 'Blinky' Example +//! +//! This application demonstrates how to control a GPIO pin on the rp235x. +//! +//! It may need to be adapted to your particular board layout and/or pin assignment. +//! +//! See the \`Cargo.toml\` file for Copyright and license details. + +#![no_std] +#![no_main] + +use defmt::*; +use defmt_rtt as _; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::OutputPin; +#[cfg(target_arch = "riscv32")] +use panic_halt as _; +#[cfg(target_arch = "arm")] +use panic_probe as _; + +// Alias for our HAL crate +use hal::entry; + +#[cfg(rp2350)] +use rp235x_hal as hal; + +#[cfg(rp2040)] +use rp2040_hal as hal; + +// use bsp::entry; +// use bsp::hal; +// use rp_pico as bsp; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +/// Note: This boot block is not necessary when using a rp-hal based BSP +/// as the BSPs already perform this step. +#[link_section = ".boot2"] +#[used] +#[cfg(rp2040)] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; + +// \`target_abi\`, \`target_arch\`, \`target_endian\`, +// \`target_env\`, \`target_family\`, \`target_feature\`, +// \`target_has_atomic\`, \`target_has_atomic_equal_alignment\`, +// \`target_has_atomic_load_store\`, \`target_os\`, +// \`target_pointer_width\`, \`target_thread_local\`, \`target_vendor\` +/// Tell the Boot ROM about our application +#[link_section = ".start_block"] +#[used] +#[cfg(rp2350)] +pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe(); + +/// External high-speed crystal on the Raspberry Pi Pico 2 board is 12 MHz. +/// Adjust if your board has a different frequency +const XTAL_FREQ_HZ: u32 = 12_000_000u32; + +/// Entry point to our bare-metal application. +/// +/// The \`#[hal::entry]\` macro ensures the Cortex-M start-up code calls this function +/// as soon as all global variables and the spinlock are initialised. +/// +/// The function configures the rp235x peripherals, then toggles a GPIO pin in +/// an infinite loop. If there is an LED connected to that pin, it will blink. +#[entry] +fn main() -> ! { + info!("Program start"); + // Grab our singleton objects + let mut pac = hal::pac::Peripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + let clocks = hal::clocks::init_clocks_and_plls( + XTAL_FREQ_HZ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .unwrap(); + + #[cfg(rp2040)] + let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); + + #[cfg(rp2350)] + let mut timer = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + + // Set the pins to their default state + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Configure GPIO25 as an output + let mut led_pin = pins.gpio25.into_push_pull_output(); + loop { + info!("on!"); + led_pin.set_high().unwrap(); + timer.delay_ms(200); + info!("off!"); + led_pin.set_low().unwrap(); + timer.delay_ms(200); + } +} + +/// Program metadata for \`picotool info\` +#[link_section = ".bi_entries"] +#[used] +pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 5] = [ + hal::binary_info::rp_cargo_bin_name!(), + hal::binary_info::rp_cargo_version!(), + hal::binary_info::rp_program_description!(c"Blinky Example"), + hal::binary_info::rp_cargo_homepage_url!(), + hal::binary_info::rp_program_build_attribute!(), +]; + +// End of file +`; + + try { + await mkdir(join(projectRoot, "src")); + await writeFile(join(projectRoot, "src", "main.rs"), mainRs); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write main.rs file", + unknownErrorToString(error) + ); + + return false; + } +} + +export enum FlashMethod { + openOCD, + picotool, +} + +/* +interface CargoTomlProfile { + "codegen-units": number; + debug: number | boolean; + "debug-assertions": boolean; + incremental?: boolean; + lto?: string; + "opt-level": number; + "overflow-checks"?: boolean; +}*/ + +interface CargoTomlDependencies { + [key: string]: + | string + | TomlInlineObject<{ + optional?: boolean; + path?: string; + version: string; + features?: string[]; + }>; +} + +interface CargoToml { + package: { + edition: string; + name: string; + version: string; + license: string; + }; + + "build-dependencies": CargoTomlDependencies; + dependencies: CargoTomlDependencies; + + /* + profile?: { + dev?: CargoTomlProfile & { "build-override"?: CargoTomlProfile }; + release?: CargoTomlProfile & { "build-override"?: CargoTomlProfile }; + test?: CargoTomlProfile & { "build-override"?: CargoTomlProfile }; + bench?: CargoTomlProfile & { "build-override"?: CargoTomlProfile }; + };*/ + + target: { + "'cfg( target_arch = \"arm\" )'": { + dependencies: CargoTomlDependencies; + }; + + "'cfg( target_arch = \"riscv32\" )'": { + dependencies: CargoTomlDependencies; + }; + + "thumbv6m-none-eabi": { + dependencies: CargoTomlDependencies; + }; + + "riscv32imac-unknown-none-elf": { + dependencies: CargoTomlDependencies; + }; + + '"thumbv8m.main-none-eabihf"': { + dependencies: CargoTomlDependencies; + }; + }; +} + +async function generateCargoToml( + projectRoot: string, + projectName: string +): Promise { + const obj: CargoToml = { + package: { + edition: "2021", + name: projectName, + version: "0.1.0", + license: "MIT or Apache-2.0", + }, + "build-dependencies": { + regex: "1.11.0", + }, + dependencies: { + "cortex-m": "0.7", + "cortex-m-rt": "0.7", + "embedded-hal": "1.0.0", + defmt: "0.3", + "defmt-rtt": "0.4", + }, + target: { + "'cfg( target_arch = \"arm\" )'": { + dependencies: { + "panic-probe": new TomlInlineObject({ + version: "0.3", + features: ["print-defmt"], + }), + }, + }, + "'cfg( target_arch = \"riscv32\" )'": { + dependencies: { + "panic-halt": new TomlInlineObject({ + version: "1.0.0", + }), + }, + }, + "thumbv6m-none-eabi": { + dependencies: { + "rp2040-hal": new TomlInlineObject({ + version: "0.11", + features: ["rt", "critical-section-impl"], + }), + "rp2040-boot2": "0.3", + }, + }, + + "riscv32imac-unknown-none-elf": { + dependencies: { + "rp235x-hal": new TomlInlineObject({ + version: "0.3", + features: ["rt", "critical-section-impl"], + }), + }, + }, + + '"thumbv8m.main-none-eabihf"': { + dependencies: { + "rp235x-hal": new TomlInlineObject({ + version: "0.3", + features: ["rt", "critical-section-impl"], + }), + }, + }, + }, + }; + + // write to file + try { + await writeTomlFile(join(projectRoot, "Cargo.toml"), obj); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write Cargo.toml file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateMemoryLayouts(projectRoot: string): Promise { + const rp2040X = `MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + /* + * Here we assume you have 2048 KiB of Flash. This is what the Pi Pico + * has, but your board may have more or less Flash and you should adjust + * this value to suit. + */ + FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + /* + * RAM consists of 4 banks, SRAM0-SRAM3, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 256K + /* + * RAM banks 4 and 5 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20040000, LENGTH = 4k + SRAM5 : ORIGIN = 0x20041000, LENGTH = 4k + + /* SRAM banks 0-3 can also be accessed directly. However, those ranges + alias with the RAM mapping, above. So don't use them at the same time! + SRAM0 : ORIGIN = 0x21000000, LENGTH = 64k + SRAM1 : ORIGIN = 0x21010000, LENGTH = 64k + SRAM2 : ORIGIN = 0x21020000, LENGTH = 64k + SRAM3 : ORIGIN = 0x21030000, LENGTH = 64k + */ +} + +EXTERN(BOOT2_FIRMWARE) + +SECTIONS { + /* ### Boot loader + * + * An executable block of code which sets up the QSPI interface for + * 'Execute-In-Place' (or XIP) mode. Also sends chip-specific commands to + * the external flash chip. + * + * Must go at the start of external flash, where the Boot ROM expects it. + */ + .boot2 ORIGIN(BOOT2) : + { + KEEP(*(.boot2)); + } > BOOT2 +} INSERT BEFORE .text; + +SECTIONS { + /* ### Boot ROM info + * + * Goes after .vector_table, to keep it in the first 512 bytes of flash, + * where picotool can find it + */ + .boot_info : ALIGN(4) + { + KEEP(*(.boot_info)); + } > FLASH + +} INSERT AFTER .vector_table; + +/* move .text to start /after/ the boot info */ +_stext = ADDR(.boot_info) + SIZEOF(.boot_info); + +SECTIONS { + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH +} INSERT AFTER .text; +`; + + const rp2350X = `MEMORY { + /* + * The RP2350 has either external or internal flash. + * + * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. + */ + FLASH : ORIGIN = 0x10000000, LENGTH = 2048K + /* + * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 512K + /* + * RAM banks 8 and 9 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K + SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K + } + + SECTIONS { + /* ### Boot ROM info + * + * Goes after .vector_table, to keep it in the first 4K of flash + * where the Boot ROM (and picotool) can find it + */ + .start_block : ALIGN(4) + { + __start_block_addr = .; + KEEP(*(.start_block)); + } > FLASH + + } INSERT AFTER .vector_table; + + /* move .text to start /after/ the boot info */ + _stext = ADDR(.start_block) + SIZEOF(.start_block); + + SECTIONS { + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH + } INSERT AFTER .text; + + SECTIONS { + /* ### Boot ROM extra info + * + * Goes after everything in our program, so it can contain a signature. + */ + .end_block : ALIGN(4) + { + __end_block_addr = .; + KEEP(*(.end_block)); + } > FLASH + + } INSERT AFTER .uninit; + + PROVIDE(start_to_end = __end_block_addr - __start_block_addr); + PROVIDE(end_to_start = __start_block_addr - __end_block_addr); + `; + + const rp2350RiscvX = `MEMORY { + /* + * The RP2350 has either external or internal flash. + * + * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. + */ + FLASH : ORIGIN = 0x10000000, LENGTH = 2048K + /* + * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 512K + /* + * RAM banks 8 and 9 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K + SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K +} + +/* # Developer notes + +- Symbols that start with a double underscore (__) are considered "private" + +- Symbols that start with a single underscore (_) are considered "semi-public"; they can be + overridden in a user linker script, but should not be referred from user code (e.g. \`extern "C" { + static mut _heap_size }\`). + +- \`EXTERN\` forces the linker to keep a symbol in the final binary. We use this to make sure a + symbol is not dropped if it appears in or near the front of the linker arguments and "it's not + needed" by any of the preceding objects (linker arguments) + +- \`PROVIDE\` is used to provide default values that can be overridden by a user linker script + +- On alignment: it's important for correctness that the VMA boundaries of both .bss and .data *and* + the LMA of .data are all \`32\`-byte aligned. These alignments are assumed by the RAM + initialization routine. There's also a second benefit: \`32\`-byte aligned boundaries + means that you won't see "Address (..) is out of bounds" in the disassembly produced by \`objdump\`. +*/ + +PROVIDE(_stext = ORIGIN(FLASH)); +PROVIDE(_stack_start = ORIGIN(RAM) + LENGTH(RAM)); +PROVIDE(_max_hart_id = 0); +PROVIDE(_hart_stack_size = 2K); +PROVIDE(_heap_size = 0); + +PROVIDE(InstructionMisaligned = ExceptionHandler); +PROVIDE(InstructionFault = ExceptionHandler); +PROVIDE(IllegalInstruction = ExceptionHandler); +PROVIDE(Breakpoint = ExceptionHandler); +PROVIDE(LoadMisaligned = ExceptionHandler); +PROVIDE(LoadFault = ExceptionHandler); +PROVIDE(StoreMisaligned = ExceptionHandler); +PROVIDE(StoreFault = ExceptionHandler); +PROVIDE(UserEnvCall = ExceptionHandler); +PROVIDE(SupervisorEnvCall = ExceptionHandler); +PROVIDE(MachineEnvCall = ExceptionHandler); +PROVIDE(InstructionPageFault = ExceptionHandler); +PROVIDE(LoadPageFault = ExceptionHandler); +PROVIDE(StorePageFault = ExceptionHandler); + +PROVIDE(SupervisorSoft = DefaultHandler); +PROVIDE(MachineSoft = DefaultHandler); +PROVIDE(SupervisorTimer = DefaultHandler); +PROVIDE(MachineTimer = DefaultHandler); +PROVIDE(SupervisorExternal = DefaultHandler); +PROVIDE(MachineExternal = DefaultHandler); + +PROVIDE(DefaultHandler = DefaultInterruptHandler); +PROVIDE(ExceptionHandler = DefaultExceptionHandler); + +/* # Pre-initialization function */ +/* If the user overrides this using the \`#[pre_init]\` attribute or by creating a \`__pre_init\` function, + then the function this points to will be called before the RAM is initialized. */ +PROVIDE(__pre_init = default_pre_init); + +/* A PAC/HAL defined routine that should initialize custom interrupt controller if needed. */ +PROVIDE(_setup_interrupts = default_setup_interrupts); + +/* # Multi-processing hook function + fn _mp_hook() -> bool; + + This function is called from all the harts and must return true only for one hart, + which will perform memory initialization. For other harts it must return false + and implement wake-up in platform-dependent way (e.g. after waiting for a user interrupt). +*/ +PROVIDE(_mp_hook = default_mp_hook); + +/* # Start trap function override + By default uses the riscv crates default trap handler + but by providing the \`_start_trap\` symbol external crates can override. +*/ +PROVIDE(_start_trap = default_start_trap); + +SECTIONS +{ + .text.dummy (NOLOAD) : + { + /* This section is intended to make _stext address work */ + . = ABSOLUTE(_stext); + } > FLASH + + .text _stext : + { + /* Put reset handler first in .text section so it ends up as the entry */ + /* point of the program. */ + KEEP(*(.init)); + KEEP(*(.init.rust)); + . = ALIGN(4); + __start_block_addr = .; + KEEP(*(.start_block)); + . = ALIGN(4); + *(.trap); + *(.trap.rust); + *(.text.abort); + *(.text .text.*); + . = ALIGN(4); + } > FLASH + + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH + + .rodata : ALIGN(4) + { + *(.srodata .srodata.*); + *(.rodata .rodata.*); + + /* 4-byte align the end (VMA) of this section. + This is required by LLD to ensure the LMA of the following .data + section will have the correct alignment. */ + . = ALIGN(4); + } > FLASH + + .data : ALIGN(32) + { + _sidata = LOADADDR(.data); + __sidata = LOADADDR(.data); + _sdata = .; + __sdata = .; + /* Must be called __global_pointer$ for linker relaxations to work. */ + PROVIDE(__global_pointer$ = . + 0x800); + *(.sdata .sdata.* .sdata2 .sdata2.*); + *(.data .data.*); + . = ALIGN(32); + _edata = .; + __edata = .; + } > RAM AT > FLASH + + .bss (NOLOAD) : ALIGN(32) + { + _sbss = .; + *(.sbss .sbss.* .bss .bss.*); + . = ALIGN(32); + _ebss = .; + } > RAM + + .end_block : ALIGN(4) + { + __end_block_addr = .; + KEEP(*(.end_block)); + } > FLASH + + /* fictitious region that represents the memory available for the heap */ + .heap (NOLOAD) : + { + _sheap = .; + . += _heap_size; + . = ALIGN(4); + _eheap = .; + } > RAM + + /* fictitious region that represents the memory available for the stack */ + .stack (NOLOAD) : + { + _estack = .; + . = ABSOLUTE(_stack_start); + _sstack = .; + } > RAM + + /* fake output .got section */ + /* Dynamic relocations are unsupported. This section is only used to detect + relocatable code in the input files and raise an error if relocatable code + is found */ + .got (INFO) : + { + KEEP(*(.got .got.*)); + } + + .eh_frame (INFO) : { KEEP(*(.eh_frame)) } + .eh_frame_hdr (INFO) : { *(.eh_frame_hdr) } +} + +PROVIDE(start_to_end = __end_block_addr - __start_block_addr); +PROVIDE(end_to_start = __start_block_addr - __end_block_addr); + + +/* Do not exceed this mark in the error messages above | */ +ASSERT(ORIGIN(FLASH) % 4 == 0, " +ERROR(riscv-rt): the start of the FLASH must be 4-byte aligned"); + +ASSERT(ORIGIN(RAM) % 32 == 0, " +ERROR(riscv-rt): the start of the RAM must be 32-byte aligned"); + +ASSERT(_stext % 4 == 0, " +ERROR(riscv-rt): \`_stext\` must be 4-byte aligned"); + +ASSERT(_sdata % 32 == 0 && _edata % 32 == 0, " +BUG(riscv-rt): .data is not 32-byte aligned"); + +ASSERT(_sidata % 32 == 0, " +BUG(riscv-rt): the LMA of .data is not 32-byte aligned"); + +ASSERT(_sbss % 32 == 0 && _ebss % 32 == 0, " +BUG(riscv-rt): .bss is not 32-byte aligned"); + +ASSERT(_sheap % 4 == 0, " +BUG(riscv-rt): start of .heap is not 4-byte aligned"); + +ASSERT(_stext + SIZEOF(.text) < ORIGIN(FLASH) + LENGTH(FLASH), " +ERROR(riscv-rt): The .text section must be placed inside the FLASH region. +Set _stext to an address smaller than 'ORIGIN(FLASH) + LENGTH(FLASH)'"); + +ASSERT(SIZEOF(.stack) > (_max_hart_id + 1) * _hart_stack_size, " +ERROR(riscv-rt): .stack section is too small for allocating stacks for all the harts. +Consider changing \`_max_hart_id\` or \`_hart_stack_size\`."); + +ASSERT(SIZEOF(.got) == 0, " +.got section detected in the input files. Dynamic relocations are not +supported. If you are linking to C code compiled using the \`gcc\` crate +then modify your build script to compile the C code _without_ the +-fPIC flag. See the documentation of the \`gcc::Config.fpic\` method for +details."); + +/* Do not exceed this mark in the error messages above | */ +`; + + try { + await writeFile(join(projectRoot, "rp2040.x"), rp2040X); + await writeFile(join(projectRoot, "rp2350.x"), rp2350X); + await writeFile(join(projectRoot, "rp2350_riscv.x"), rp2350RiscvX); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write memory.x files", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateBuildRs(projectRoot: string): Promise { + const buildRs = `//! Set up linker scripts for the rp235x-hal examples + +use std::fs::{ File, read_to_string }; +use std::io::Write; +use std::path::PathBuf; + +use regex::Regex; + +fn main() { + println!("cargo::rustc-check-cfg=cfg(rp2040)"); + println!("cargo::rustc-check-cfg=cfg(rp2350)"); + + // Put the linker script somewhere the linker can find it + let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + println!("cargo:rustc-link-search={}", out.display()); + + println!("cargo:rerun-if-changed=.pico-rs"); + let contents = read_to_string(".pico-rs") + .map(|s| s.trim().to_string().to_lowercase()) + .unwrap_or_else(|e| { + eprintln!("Failed to read file: {}", e); + String::new() + }); + + // The file \`memory.x\` is loaded by cortex-m-rt's \`link.x\` script, which + // is what we specify in \`.cargo/config.toml\` for Arm builds + let target; + if contents == "rp2040" { + target = "thumbv6m-none-eabi"; + let memory_x = include_bytes!("rp2040.x"); + let mut f = File::create(out.join("memory.x")).unwrap(); + f.write_all(memory_x).unwrap(); + println!("cargo::rustc-cfg=rp2040"); + println!("cargo:rerun-if-changed=rp2040.x"); + } else { + if contents.contains("riscv") { + target = "riscv32imac-unknown-none-elf"; + } else { + target = "thumbv8m.main-none-eabihf"; + } + let memory_x = include_bytes!("rp2350.x"); + let mut f = File::create(out.join("memory.x")).unwrap(); + f.write_all(memory_x).unwrap(); + println!("cargo::rustc-cfg=rp2350"); + println!("cargo:rerun-if-changed=rp2350.x"); + } + + let re = Regex::new(r"target = .*").unwrap(); + let config_toml = include_str!(".cargo/config.toml"); + let result = re.replace(config_toml, format!("target = \\"{}\\"", target)); + let mut f = File::create(".cargo/config.toml").unwrap(); + f.write_all(result.as_bytes()).unwrap(); + + // The file \`rp2350_riscv.x\` is what we specify in \`.cargo/config.toml\` for + // RISC-V builds + let rp2350_riscv_x = include_bytes!("rp2350_riscv.x"); + let mut f = File::create(out.join("rp2350_riscv.x")).unwrap(); + f.write_all(rp2350_riscv_x).unwrap(); + println!("cargo:rerun-if-changed=rp2350_riscv.x"); + + println!("cargo:rerun-if-changed=build.rs"); +} +`; + + try { + await writeFile(join(projectRoot, "build.rs"), buildRs); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write build.rs file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateGitIgnore(projectRoot: string): Promise { + const gitIgnore = `# Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode,macos,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux +`; + + try { + await writeFile(join(projectRoot, ".gitignore"), gitIgnore); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write .gitignore file", + unknownErrorToString(error) + ); + + return false; + } +} + +/** + * Note: requires PICOTOOL_PATH to be set in the environment when running cargo. + * + * @param projectRoot The path where the project folder should be generated. + */ +async function generateCargoConfig(projectRoot: string): Promise { + const cargoConfig = `# +# Cargo Configuration for the https://github.com/rp-rs/rp-hal.git repository. +# +# You might want to make a similar file in your own repository if you are +# writing programs for Raspberry Silicon microcontrollers. +# + +[build] +target = "thumbv8m.main-none-eabihf" +# Set the default target to match the Cortex-M33 in the RP2350 +# target = "thumbv8m.main-none-eabihf" +# target = "thumbv6m-none-eabi" +# target = "riscv32imac-unknown-none-elf" + +# Target specific options +[target.thumbv6m-none-eabi] +# Pass some extra options to rustc, some of which get passed on to the linker. +# +# * linker argument --nmagic turns off page alignment of sections (which saves +# flash space) +# * linker argument -Tlink.x tells the linker to use link.x as the linker +# script. This is usually provided by the cortex-m-rt crate, and by default +# the version in that crate will include a file called \`memory.x\` which +# describes the particular memory layout for your specific chip. +# * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't +# have SIMD) +rustflags = [ + "-C", "linker=flip-link", + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "no-vectorize-loops", +] + +# Use picotool for loading. +# +# Load an elf, skipping unchanged flash sectors, verify it, and execute it +runner = "\${PICOTOOL_PATH} load -u -v -x -t elf" +#runner = "probe-rs run --chip \${CHIP} --protocol swd" + +# This is the hard-float ABI for Arm mode. +# +# The FPU is enabled by default, and float function arguments use FPU +# registers. +[target.thumbv8m.main-none-eabihf] +# Pass some extra options to rustc, some of which get passed on to the linker. +# +# * linker argument --nmagic turns off page alignment of sections (which saves +# flash space) +# * linker argument -Tlink.x tells the linker to use link.x as a linker script. +# This is usually provided by the cortex-m-rt crate, and by default the +# version in that crate will include a file called \`memory.x\` which describes +# the particular memory layout for your specific chip. +# * linker argument -Tdefmt.x also tells the linker to use \`defmt.x\` as a +# secondary linker script. This is required to make defmt_rtt work. +rustflags = [ + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "target-cpu=cortex-m33", +] + +# Use picotool for loading. +# +# Load an elf, skipping unchanged flash sectors, verify it, and execute it +runner = "\${PICOTOOL_PATH} load -u -v -x -t elf" +#runner = "probe-rs run --chip \${CHIP} --protocol swd" + +# This is the soft-float ABI for RISC-V mode. +# +# Hazard 3 does not have an FPU and so float function arguments use integer +# registers. +[target.riscv32imac-unknown-none-elf] +# Pass some extra options to rustc, some of which get passed on to the linker. +# +# * linker argument --nmagic turns off page alignment of sections (which saves +# flash space) +# * linker argument -Trp235x_riscv.x also tells the linker to use +# \`rp235x_riscv.x\` as a linker script. This adds in RP2350 RISC-V specific +# things that the riscv-rt crate's \`link.x\` requires and then includes +# \`link.x\` automatically. This is the reverse of how we do it on Cortex-M. +# * linker argument -Tdefmt.x also tells the linker to use \`defmt.x\` as a +# secondary linker script. This is required to make defmt_rtt work. +rustflags = [ + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Trp2350_riscv.x", + "-C", "link-arg=-Tdefmt.x", +] + +# Use picotool for loading. +# +# Load an elf, skipping unchanged flash sectors, verify it, and execute it +runner = "\${PICOTOOL_PATH} load -u -v -x -t elf" +#runner = "probe-rs run --chip \${CHIP} --protocol swd" + +[env] +DEFMT_LOG = "debug" +`; + + try { + await mkdir(join(projectRoot, ".cargo"), { recursive: true }); + await writeFile(join(projectRoot, ".cargo", "config.toml"), cargoConfig); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write .cargo/config.toml file", + unknownErrorToString(error) + ); + + return false; + } +} + +/** + * Generates a new Rust project. + * + * @param projectRoot The path where the project folder should be generated. + * @param projectName The name of the project. + * @param flashMethod The flash method to use. + * @returns A promise that resolves to true if the project was generated successfully. + */ +export async function generateRustProject( + projectFolder: string, + projectName: string +): Promise { + const picotoolPath: string | undefined = await commands.executeCommand( + `${extensionName}.${GetPicotoolPathCommand.id}` + ); + + if (picotoolPath === undefined) { + Logger.error(LoggerSource.projectRust, "Failed to get picotool path."); + + void window.showErrorMessage( + "Failed to detect or install picotool. Please try again and check your settings." + ); + + return false; + } + const picotoolVersion = picotoolPath.match( + /picotool[/\\]+(\d+\.\d+\.\d+)/ + )?.[1]; + + if (!picotoolVersion) { + Logger.error( + LoggerSource.projectRust, + "Failed to detect picotool version." + ); + + void window.showErrorMessage( + "Failed to detect picotool version. Please try again and check your settings." + ); + + return false; + } + + try { + await mkdir(projectFolder, { recursive: true }); + } catch (error) { + const msg = unknownErrorToString(error); + if ( + msg.includes("EPERM") || + msg.includes("EACCES") || + msg.includes("access denied") + ) { + Logger.error( + LoggerSource.projectRust, + "Failed to create project folder", + "Permission denied. Please check your permissions." + ); + + void window.showErrorMessage( + "Failed to create project folder. Permission denied - Please check your permissions." + ); + } else { + Logger.error( + LoggerSource.projectRust, + "Failed to create project folder", + unknownErrorToString(error) + ); + + void window.showErrorMessage( + "Failed to create project folder. See the output panel for more details." + ); + } + + return false; + } + + // TODO: do all in parallel + let result = await generateCargoToml(projectFolder, projectName); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate Cargo.toml file" + ); + + return false; + } + + result = await generateMemoryLayouts(projectFolder); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate memory.x files"); + + return false; + } + + result = await generateBuildRs(projectFolder); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate build.rs file"); + + return false; + } + + result = await generateGitIgnore(projectFolder); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate .gitignore file" + ); + + return false; + } + + result = await generateCargoConfig(projectFolder); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate .cargo/config.toml file" + ); + + return false; + } + + result = await generateMainRs(projectFolder); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate main.rs file"); + + return false; + } + + result = await generateVSCodeConfig(projectFolder); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate .vscode configuration files." + ); + + return false; + } + + // add .pico-rs file + try { + // TODO: dynamic selection of RP2040 or RP2350 (risc-v or arm) + await writeFile(join(projectFolder, ".pico-rs"), "rp2350"); + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write .pico-rs file", + unknownErrorToString(error) + ); + + return false; + } + + return true; +} diff --git a/src/utils/projectGeneration/tomlUtil.mts b/src/utils/projectGeneration/tomlUtil.mts new file mode 100644 index 00000000..5d684ad7 --- /dev/null +++ b/src/utils/projectGeneration/tomlUtil.mts @@ -0,0 +1,100 @@ +import { assert } from "console"; +import { writeFile } from "fs/promises"; + +export class TomlInlineObject> { + constructor(public values: T) {} + public toString(): string { + return `{ ${Object.entries(this.values) + .filter(([, value]) => value !== null && value !== undefined) + .map(([key, value]) => { + if (Array.isArray(value)) { + return `${key} = ${tomlArrayToInlineString(value)}`; + } + + assert( + typeof value !== "object", + "TomlInlineObject Value must not be an object." + ); + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${key} = ${typeof value === "string" ? `"${value}"` : value}`; + }) + .join(", ")} }`; + } +} + +function tomlArrayToInlineString(value: T[]): string { + return `[${value + .map(v => (typeof v === "string" ? `"${v}"` : (v as number | boolean))) + .join(", ")}]`; +} + +function tomlObjectToString( + value: object | Record, + parent = "" +): string { + return ( + Object.entries(value) + .filter(([, value]) => value !== null && value !== undefined) + // sort entries by type of value (object type last) + .sort(([, value1], [, value2]) => + typeof value1 === "object" && !(value1 instanceof Array) + ? 1 + : typeof value2 === "object" && !(value2 instanceof Array) + ? -1 + : 0 + ) + .reduce((acc, [key, value]) => { + if (value instanceof TomlInlineObject) { + acc += `${key} = ${value.toString()}\n`; + } else if (Array.isArray(value)) { + acc += `${key} = ${tomlArrayToInlineString(value)}\n`; + } else if (typeof value === "object") { + // check if every subkeys value is of type object + if ( + Object.entries(value as object).every( + ([, value]) => + !(value instanceof TomlInlineObject) && + typeof value === "object" && + !Array.isArray(value) + ) + ) { + acc += tomlObjectToString(value as object, parent + key + "."); + + return acc; + } + + acc += `${acc.split("\n")[0].split(".").length <= 1 ? "\n" : ""}[${ + parent + key + }]\n`; + acc += tomlObjectToString(value as object, parent + key + "."); + } else { + acc += `${key} = ${ + typeof value === "string" ? `"${value}"` : value + }\n`; + } + + return acc; + }, "") + ); +} + +/** + * Writes a toml object to a file. + * + * Please note there are special types for + * writing an object as inline object or writing a dictionary. + * + * @param filePath The path to the file. + * @param toml The toml object. + * @returns A promise that resolves when the file was written. + */ +export async function writeTomlFile( + filePath: string, + toml: object +): Promise { + const tomlString = tomlObjectToString(toml); + + // write to file + return writeFile(filePath, tomlString); +} diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts new file mode 100644 index 00000000..c43647ad --- /dev/null +++ b/src/utils/rustUtil.mts @@ -0,0 +1,582 @@ +import { readFileSync, writeFileSync } from "fs"; +import Logger, { LoggerSource } from "../logger.mjs"; +import { unknownErrorToString } from "./errorHelper.mjs"; +import { env, ProgressLocation, Uri, window } from "vscode"; +import { promisify } from "util"; +import { exec } from "child_process"; +import { join } from "path"; + +/*const STABLE_INDEX_DOWNLOAD_URL = + "https://static.rust-lang.org/dist/channel-rust-stable.toml";*/ + +const execAsync = promisify(exec); + +export enum FlashMethod { + debugProbe = 0, + elf2Uf2 = 1, + cargoEmbed = 2, +} + +/* +interface IndexToml { + pkg?: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "rust-std"?: { + target?: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "thumbv6m-none-eabi"?: { + available: boolean; + // eslint-disable-next-line @typescript-eslint/naming-convention + xz_url: string; + }; + }; + }; + // eslint-disable-next-line @typescript-eslint/naming-convention + "rust-analysis"?: { + target?: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "thumbv6m-none-eabi"?: { + available: boolean; + // eslint-disable-next-line @typescript-eslint/naming-convention + xz_url: string; + }; + }; + }; + }; +} + +function computeDownloadLink(release: string): string { + let platform = ""; + switch (process.platform) { + case "darwin": + platform = "apple-darwin"; + break; + case "linux": + platform = "unknown-linux-gnu"; + break; + case "win32": + // maybe gnu in the future and point to arm embedded toolchain + platform = "pc-windows-msvc"; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + let arch = ""; + switch (process.arch) { + case "x64": + arch = "x86_64"; + break; + case "arm64": + arch = "aarch64"; + break; + default: + throw new Error(`Unsupported architecture: ${process.arch}`); + } + + return ( + "https://static.rust-lang.org/dist" + + `/rust-${release}-${arch}-${platform}.tar.xz` + ); +}*/ + +export async function cargoInstall( + packageName: string, + locked = false +): Promise { + const command = process.platform === "win32" ? "cargo.exe" : "cargo"; + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stdout, stderr } = await execAsync( + `${command} install ${locked ? "--locked " : ""}${packageName}`, + { + windowsHide: true, + } + ); + + return true; + } catch (error) { + const msg = unknownErrorToString(error); + if ( + msg.toLowerCase().includes("already exists") || + msg.toLowerCase().includes("to your path") || + msg.toLowerCase().includes("is already installed") || + msg.toLowerCase().includes("yanked in registry") + ) { + Logger.warn( + LoggerSource.rustUtil, + `Cargo package '${packageName}' is already installed ` + + "or cargo bin not in PATH:", + msg + ); + + return true; + } + Logger.error( + LoggerSource.rustUtil, + `Failed to install cargo package '${packageName}': ${unknownErrorToString( + error + )}` + ); + + return false; + } +} + +export function calculateRequiredHostTriple(): string { + const arch = process.arch; + const platform = process.platform; + let triple = ""; + if (platform === "win32" && arch === "x64") { + triple = "x86_64-pc-windows-msvc"; + } else if (platform === "darwin") { + if (arch === "x64") { + triple = "x86_64-apple-darwin"; + } else { + triple = "aarch64-apple-darwin"; + } + } else if (platform === "linux") { + if (arch === "x64") { + triple = "x86_64-unknown-linux-gnu"; + } else if (arch === "arm64") { + triple = "aarch64-unknown-linux-gnu"; + } else { + throw new Error(`Unsupported architecture: ${arch}`); + } + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + return triple; +} + +async function checkHostToolchainInstalled(): Promise { + try { + const hostTriple = calculateRequiredHostTriple(); + const rustup = process.platform === "win32" ? "rustup.exe" : "rustup"; + const { stdout } = await execAsync(`${rustup} toolchain list`, { + windowsHide: true, + }); + + return stdout.includes(hostTriple); + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to check for host toolchain: ${unknownErrorToString(error)}` + ); + + return false; + } + + // or else check .rustup/toolchains/ for the host toolchain +} + +export async function installHostToolchain(): Promise { + try { + // TODO: maybe listen for stderr + // this will automatically take care of having the correct + // recommended host toolchain installed except for snap rustup installs + const { stdout } = await execAsync("rustup show", { + windowsHide: true, + }); + + if (stdout.includes("no active toolchain")) { + await execAsync("rustup default stable", { + windowsHide: true, + }); + } + + return true; + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to install host toolchain: ${unknownErrorToString(error)}` + ); + + return false; + } +} + +/** + * Checks for all requirements except targets and cargo packages. + * + * (Cares about UI feedback) + * + * @returns {boolean} True if all requirements are met, false otherwise. + */ +export async function checkRustInstallation(): Promise { + let rustupOk = false; + let rustcOk = false; + let cargoOk = false; + try { + const rustup = process.platform === "win32" ? "rustup.exe" : "rustup"; + await execAsync(`${rustup} --version`, { + windowsHide: true, + }); + rustupOk = true; + + // check rustup toolchain + const result = await checkHostToolchainInstalled(); + if (!result) { + Logger.error(LoggerSource.rustUtil, "Host toolchain not installed"); + + // TODO: make cancelable (Ctrl+C) + const result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing Rust toolchain", + cancellable: true, + }, + async () => installHostToolchain() + ); + + if (!result) { + void window.showErrorMessage( + "Failed to install Rust toolchain. " + + "Please install it manually with `rustup show`." + ); + + return false; + } + } + + const rustc = process.platform === "win32" ? "rustc.exe" : "rustc"; + await execAsync(`${rustc} --version`, { + windowsHide: true, + }); + rustcOk = true; + + const cargo = process.platform === "win32" ? "cargo.exe" : "cargo"; + await execAsync(`${cargo} --version`, { + windowsHide: true, + }); + cargoOk = true; + + return true; + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Rust installation check failed: ${unknownErrorToString(error)}` + ); + + if (!rustupOk) { + void window + .showErrorMessage( + "Rustup is not installed. Please install it manually.", + "Install" + ) + .then(result => { + if (result) { + env.openExternal( + Uri.parse("https://www.rust-lang.org/tools/install", true) + ); + } + }); + } else if (!rustcOk) { + void window.showErrorMessage( + "Rustc is not installed. Please install it manually." + ); + } else if (!cargoOk) { + void window.showErrorMessage( + "Cargo is not installed. Please install it manually." + ); + } else { + void window.showErrorMessage( + "Failed to check Rust installation. Please check the logs." + ); + } + + return false; + } +} + +/** + * Installs all requirements for embedded Rust development. + * (if required) + * + * @returns {boolean} True if all requirements are met or have been installed, false otherwise. + */ +export async function downloadAndInstallRust(): Promise { + let result = await checkRustInstallation(); + if (!result) { + return false; + } + + // install targets + const targets = [ + "thumbv6m-none-eabi", + "thumbv8m.main-none-eabihf", + "riscv32imac-unknown-none-elf", + ]; + for (const target of targets) { + try { + const rustup = process.platform === "win32" ? "rustup.exe" : "rustup"; + await execAsync(`${rustup} target add ${target}`, { + windowsHide: true, + }); + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to install target '${target}': ${unknownErrorToString(error)}` + ); + + void window.showErrorMessage( + `Failed to install target '${target}'. Please check the logs.` + ); + + return false; + } + } + + // install flip-link + const flipLink = "flip-link"; + result = await cargoInstall(flipLink, false); + if (!result) { + void window.showErrorMessage( + `Failed to install cargo package '${flipLink}'.` + + "Please check the logs." + ); + + return false; + } + + // or install probe-rs-tools + const probeRsTools = "defmt-print"; + result = await cargoInstall(probeRsTools, true); + if (!result) { + void window.showErrorMessage( + `Failed to install cargo package '${probeRsTools}'.` + + "Please check the logs." + ); + + return false; + } + + // install elf2uf2-rs + const elf2uf2Rs = "elf2uf2-rs"; + result = await cargoInstall(elf2uf2Rs, true); + if (!result) { + void window.showErrorMessage( + `Failed to install cargo package '${elf2uf2Rs}'.` + + "Please check the logs." + ); + + return false; + } + + // install cargo-generate binary + /*result = await installCargoGenerate(); + if (!result) { + void window.showErrorMessage( + "Failed to install cargo-generate. Please check the logs." + ); + + return false; + }*/ + + return true; +} + +/* +function platformToGithubMatrix(platform: string): string { + switch (platform) { + case "darwin": + return "macos-latest"; + case "linux": + return "ubuntu-latest"; + case "win32": + return "windows-latest"; + default: + throw new Error(`Unsupported platform: ${platform}`); + } +} + +function archToGithubMatrix(arch: string): string { + switch (arch) { + case "x64": + return "x86_64"; + case "arm64": + return "aarch64"; + default: + throw new Error(`Unsupported architecture: ${arch}`); + } +} + +async function installCargoGenerate(): Promise { + const release = await getRustToolsReleases(); + if (!release) { + Logger.error(LoggerSource.rustUtil, "Failed to get Rust tools releases"); + + return false; + } + + const assetName = `cargo-generate-${platformToGithubMatrix( + process.platform + )}-${archToGithubMatrix(process.arch)}.zip`; + + const tmpLoc = join(tmpdir(), "pico-vscode-rs"); + + const result = await downloadAndInstallGithubAsset( + release[0], + release[0], + GithubRepository.rsTools, + tmpLoc, + "cargo-generate.zip", + assetName, + "cargo-generate" + ); + + if (!result) { + Logger.error(LoggerSource.rustUtil, "Failed to install cargo-generate"); + + return false; + } + + const cargoBin = join(homedir(), ".cargo", "bin"); + + try { + mkdirSync(cargoBin, { recursive: true }); + renameSync( + join( + tmpLoc, + "cargo-generate" + (process.platform === "win32" ? ".exe" : "") + ), + join( + cargoBin, + "cargo-generate" + (process.platform === "win32" ? ".exe" : "") + ) + ); + + if (process.platform !== "win32") { + await execAsync(`chmod +x ${join(cargoBin, "cargo-generate")}`, { + windowsHide: true, + }); + } + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to move cargo-generate to ~/.cargo/bin: ${unknownErrorToString( + error + )}` + ); + + return false; + } + + return true; +}*/ + +/* +function flashMethodToArg(fm: FlashMethod): string { + switch (fm) { + case FlashMethod.cargoEmbed: + case FlashMethod.debugProbe: + return "probe-rs"; + case FlashMethod.elf2Uf2: + return "elf2uf2-rs"; + } +} + + +export async function generateRustProject( + projectFolder: string, + name: string, + flashMethod: FlashMethod +): Promise { + try { + const valuesFile = join(tmpdir(), "pico-vscode", "values.toml"); + await workspace.fs.createDirectory(Uri.file(dirname(valuesFile))); + await workspace.fs.writeFile( + Uri.file(valuesFile), + // TODO: make selectable in UI + Buffer.from( + `[values]\nflash_method="${flashMethodToArg(flashMethod)}"\n`, + "utf-8" + ) + ); + + // TODO: fix outside function (maybe) + let projectRoot = projectFolder.replaceAll("\\", "/"); + if (projectRoot.endsWith(name)) { + projectRoot = projectRoot.slice(0, projectRoot.length - name.length); + } + + // cache template and use --path + const command = + "cargo generate --git " + + "https://github.com/rp-rs/rp2040-project-template " + + ` --name ${name} --values-file "${valuesFile}" ` + + `--destination "${projectRoot}"`; + + const customEnv = { ...process.env }; + customEnv["PATH"] += `${process.platform === "win32" ? ";" : ":"}${join( + homedir(), + ".cargo", + "bin" + )}`; + // TODO: add timeout + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stdout, stderr } = await execAsync(command, { + windowsHide: true, + env: customEnv, + }); + + if (stderr) { + Logger.error( + LoggerSource.rustUtil, + `Failed to generate Rust project: ${stderr}` + ); + + return false; + } + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to generate Rust project: ${unknownErrorToString(error)}` + ); + + return false; + } + + return true; +}*/ + +/** + * Get the selected chip from the .pico-rs file in the workspace folder. + * + * @param workspaceFolder The workspace folder path. + * @returns Returns the selected chip or null if the file does not exist or is invalid. + */ +export function rustProjectGetSelectedChip( + workspaceFolder: string +): "rp2040" | "rp2350" | "rp2350-riscv" | null { + const picors = join(workspaceFolder, ".pico-rs"); + + try { + const contents = readFileSync(picors, "utf-8").trim(); + + if ( + contents !== "rp2040" && + contents !== "rp2350" && + contents !== "rp2350-riscv" + ) { + Logger.error( + LoggerSource.rustUtil, + `Invalid chip in .pico-rs: ${contents}` + ); + + // reset to rp2040 + writeFileSync(picors, "rp2040", "utf-8"); + + return "rp2040"; + } + + return contents; + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to read .pico-rs: ${unknownErrorToString(error)}` + ); + + return null; + } +} diff --git a/src/utils/sharedConstants.mts b/src/utils/sharedConstants.mts index 48de2904..690d90c2 100644 --- a/src/utils/sharedConstants.mts +++ b/src/utils/sharedConstants.mts @@ -5,3 +5,4 @@ export const WINDOWS_ARM64_PYTHON_DOWNLOAD_URL = export const CURRENT_PYTHON_VERSION = "3.12.6"; export const CURRENT_DATA_VERSION = "0.17.0"; +export const OPENOCD_VERSION = "0.12.0+dev"; diff --git a/src/utils/versionBundles.mts b/src/utils/versionBundles.mts index ae210c27..56260b8e 100644 --- a/src/utils/versionBundles.mts +++ b/src/utils/versionBundles.mts @@ -4,6 +4,7 @@ import { isInternetConnected } from "./downloadHelpers.mjs"; import { get } from "https"; import Logger from "../logger.mjs"; import { CURRENT_DATA_VERSION } from "./sharedConstants.mjs"; +import { compare } from "./semverUtil.mjs"; const versionBundlesUrl = "https://raspberrypi.github.io/pico-vscode/" + @@ -106,6 +107,26 @@ export default class VersionBundlesLoader { return (this.bundles ?? {})[version]; } + public async getLatest(): Promise { + if (this.bundles === undefined) { + await this.loadBundles(); + } + + return Object.entries(this.bundles ?? {}).sort( + (a, b) => compare(a[0], b[0]) * -1 + )[0][1]; + } + + public async getLatestSDK(): Promise { + if (this.bundles === undefined) { + await this.loadBundles(); + } + + return Object.entries(this.bundles ?? {}).sort( + (a, b) => compare(a[0], b[0]) * -1 + )[0][0]; + } + public async getPythonWindowsAmd64Url( pythonVersion: string ): Promise { diff --git a/src/webview/activityBar.mts b/src/webview/activityBar.mts index 1035f708..c64602bf 100644 --- a/src/webview/activityBar.mts +++ b/src/webview/activityBar.mts @@ -44,6 +44,7 @@ const DOCUMENTATION_COMMANDS_PARENT_LABEL = "Documentation"; const NEW_C_CPP_PROJECT_LABEL = "New C/C++ Project"; const NEW_MICROPYTHON_PROJECT_LABEL = "New MicroPython Project"; +const NEW_RUST_PROJECT_LABEL = "New Rust Project"; const IMPORT_PROJECT_LABEL = "Import Project"; const EXAMPLE_PROJECT_LABEL = "New Project From Example"; const SWITCH_SDK_LABEL = "Switch SDK"; @@ -107,6 +108,9 @@ export class PicoProjectActivityBar case NEW_MICROPYTHON_PROJECT_LABEL: element.iconPath = new ThemeIcon("file-directory-create"); break; + case NEW_RUST_PROJECT_LABEL: + element.iconPath = new ThemeIcon("file-directory-create"); + break; case IMPORT_PROJECT_LABEL: // alt. "repo-pull" element.iconPath = new ThemeIcon("repo-clone"); @@ -207,6 +211,15 @@ export class PicoProjectActivityBar arguments: [ProjectLang.micropython], } ), + new QuickAccessCommand( + NEW_RUST_PROJECT_LABEL, + TreeItemCollapsibleState.None, + { + command: `${extensionName}.${NewProjectCommand.id}`, + title: NEW_RUST_PROJECT_LABEL, + arguments: [ProjectLang.rust], + } + ), new QuickAccessCommand( IMPORT_PROJECT_LABEL, TreeItemCollapsibleState.None, diff --git a/src/webview/newProjectPanel.mts b/src/webview/newProjectPanel.mts index 6cdbd68a..a465a121 100644 --- a/src/webview/newProjectPanel.mts +++ b/src/webview/newProjectPanel.mts @@ -61,12 +61,11 @@ import { import { unknownErrorToString } from "../utils/errorHelper.mjs"; import type { Progress as GotProgress } from "got"; import findPython, { showPythonNotFoundError } from "../utils/pythonHelper.mjs"; +import { OPENOCD_VERSION } from "../utils/sharedConstants.mjs"; export const NINJA_AUTO_INSTALL_DISABLED = false; // process.platform === "linux" && process.arch === "arm64"; -export const openOCDVersion = "0.12.0+dev"; - interface ImportProjectMessageValue { selectedSDK: string; selectedToolchain: string; @@ -1054,7 +1053,7 @@ export class NewProjectPanel { }, async progress2 => { const result = await downloadAndInstallOpenOCD( - openOCDVersion, + OPENOCD_VERSION, (prog: GotProgress) => { const per = prog.percent * 100; progress2.report({ @@ -1305,7 +1304,7 @@ export class NewProjectPanel { sdkVersion: selectedSDK, sdkPath: buildSDKPath(selectedSDK), picotoolVersion: selectedPicotool, - openOCDVersion: openOCDVersion, + openOCDVersion: OPENOCD_VERSION, }, ninjaExecutable, cmakeExecutable, @@ -1330,7 +1329,7 @@ export class NewProjectPanel { sdkVersion: selectedSDK, sdkPath: buildSDKPath(selectedSDK), picotoolVersion: selectedPicotool, - openOCDVersion: openOCDVersion, + openOCDVersion: OPENOCD_VERSION, }, ninjaExecutable, cmakeExecutable, @@ -1351,7 +1350,7 @@ export class NewProjectPanel { sdkVersion: selectedSDK, sdkPath: buildSDKPath(selectedSDK), picotoolVersion: selectedPicotool, - openOCDVersion: openOCDVersion, + openOCDVersion: OPENOCD_VERSION, }, ninjaExecutable, cmakeExecutable, diff --git a/src/webview/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts new file mode 100644 index 00000000..2e77c6fd --- /dev/null +++ b/src/webview/newRustProjectPanel.mts @@ -0,0 +1,583 @@ +/* eslint-disable max-len */ +import type { Webview, Progress } from "vscode"; +import { + Uri, + ViewColumn, + window, + type WebviewPanel, + type Disposable, + ColorThemeKind, + workspace, + ProgressLocation, + commands, +} from "vscode"; +import Settings from "../settings.mjs"; +import Logger from "../logger.mjs"; +import type { WebviewMessage } from "./newProjectPanel.mjs"; +import { + getNonce, + getProjectFolderDialogOptions, + getWebviewOptions, +} from "./newProjectPanel.mjs"; +import { existsSync } from "fs"; +import { join } from "path"; +import { unknownErrorToString } from "../utils/errorHelper.mjs"; +import { downloadAndInstallRust } from "../utils/rustUtil.mjs"; +import { + type FlashMethod, + generateRustProject, +} from "../utils/projectGeneration/projectRust.mjs"; +import { installLatestRustRequirements } from "../utils/download.mjs"; + +interface SubmitMessageValue { + projectName: string; + flashMethod: FlashMethod; +} + +export class NewRustProjectPanel { + public static currentPanel: NewRustProjectPanel | undefined; + + public static readonly viewType = "newPicoRustProject"; + + private readonly _panel: WebviewPanel; + private readonly _extensionUri: Uri; + private readonly _settings: Settings; + private readonly _logger: Logger = new Logger("NewRustProjectPanel"); + private _disposables: Disposable[] = []; + + private _projectRoot?: Uri; + + public static createOrShow(extensionUri: Uri, projectUri?: Uri): void { + const column = window.activeTextEditor + ? window.activeTextEditor.viewColumn + : undefined; + + if (NewRustProjectPanel.currentPanel) { + NewRustProjectPanel.currentPanel._panel.reveal(column); + // update already exiting panel with new project root + if (projectUri) { + NewRustProjectPanel.currentPanel._projectRoot = projectUri; + // update webview + void NewRustProjectPanel.currentPanel._panel.webview.postMessage({ + command: "changeLocation", + value: projectUri?.fsPath, + }); + } + + return; + } + + const panel = window.createWebviewPanel( + NewRustProjectPanel.viewType, + "New Rust Pico Project", + column || ViewColumn.One, + getWebviewOptions(extensionUri) + ); + + const settings = Settings.getInstance(); + if (!settings) { + panel.dispose(); + + void window + .showErrorMessage( + "Failed to load settings. Please restart VS Code or reload the window.", + "Reload Window" + ) + .then(selected => { + if (selected === "Reload Window") { + commands.executeCommand("workbench.action.reloadWindow"); + } + }); + + return; + } + + NewRustProjectPanel.currentPanel = new NewRustProjectPanel( + panel, + settings, + extensionUri, + projectUri + ); + } + + public static revive(panel: WebviewPanel, extensionUri: Uri): void { + const settings = Settings.getInstance(); + if (settings === undefined) { + // TODO: maybe add restart button + void window.showErrorMessage( + "Failed to load settings. Please restart VSCode." + ); + + return; + } + + // TODO: reload if it was import panel maybe in state + NewRustProjectPanel.currentPanel = new NewRustProjectPanel( + panel, + settings, + extensionUri + ); + } + + private constructor( + panel: WebviewPanel, + settings: Settings, + extensionUri: Uri, + projectUri?: Uri + ) { + this._panel = panel; + this._extensionUri = extensionUri; + this._settings = settings; + + this._projectRoot = projectUri ?? this._settings.getLastProjectRoot(); + + void this._update(); + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // Update the content based on view changes + this._panel.onDidChangeViewState( + async () => { + if (this._panel.visible) { + await this._update(); + } + }, + null, + this._disposables + ); + + workspace.onDidChangeConfiguration( + async () => { + await this._updateTheme(); + }, + null, + this._disposables + ); + + this._panel.webview.onDidReceiveMessage( + async (message: WebviewMessage) => { + switch (message.command) { + case "changeLocation": + { + const newLoc = await window.showOpenDialog( + getProjectFolderDialogOptions(this._projectRoot, false) + ); + + if (newLoc && newLoc[0]) { + // overwrite preview folderUri + this._projectRoot = newLoc[0]; + await this._settings.setLastProjectRoot(newLoc[0]); + + // update webview + await this._panel.webview.postMessage({ + command: "changeLocation", + value: newLoc[0].fsPath, + }); + } + } + break; + case "cancel": + this.dispose(); + break; + case "error": + void window.showErrorMessage(message.value as string); + break; + case "submit": + { + const data = message.value as SubmitMessageValue; + + if ( + this._projectRoot === undefined || + this._projectRoot.fsPath === "" + ) { + void window.showErrorMessage( + "No project root selected. Please select a project root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + if ( + data.projectName === undefined || + data.projectName.length === 0 + ) { + void window.showWarningMessage( + "The project name is empty. Please enter a project name." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // check if projectRoot/projectName folder already exists + if ( + existsSync(join(this._projectRoot.fsPath, data.projectName)) + ) { + void window.showErrorMessage( + "Project already exists. " + + "Please select a different project name or root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // close panel before generating project + this.dispose(); + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Generating Rust Pico project ${ + data.projectName ?? "undefined" + } in ${this._projectRoot?.fsPath}...`, + }, + async progress => + this._generateProjectOperation(progress, data, message) + ); + } + break; + } + }, + null, + this._disposables + ); + + if (projectUri !== undefined) { + // update webview + void this._panel.webview.postMessage({ + command: "changeLocation", + value: projectUri.fsPath, + }); + } + } + + private async _generateProjectOperation( + progress: Progress<{ message?: string; increment?: number }>, + data: SubmitMessageValue, + message: WebviewMessage + ): Promise { + const projectPath = this._projectRoot?.fsPath ?? ""; + + if ( + typeof message.value !== "object" || + message.value === null || + projectPath.length === 0 + ) { + void window.showErrorMessage( + "Failed to generate MicroPython project. " + + "Please try again and check your settings." + ); + + return; + } + + // install rust (if necessary) + const cargo = await downloadAndInstallRust(); + if (!cargo) { + void window.showErrorMessage( + "Failed to install Rust. Please try again and check your settings." + ); + + return; + } + + let result = await installLatestRustRequirements(this._extensionUri); + + if (!result) { + return; + } + + const projectFolder = join(projectPath, data.projectName); + + result = await generateRustProject( + projectFolder, + data.projectName + //data.flashMethod + ); + + if (!result) { + this._logger.error("Failed to generate Rust project."); + + void window.setStatusBarMessage( + "Failed to generate Rust project. See the output panel for more details.", + 7000 + ); + + return; + } + + // wait 2 seconds to give user option to read notifications + await new Promise(resolve => setTimeout(resolve, 2000)); + + // open and call initialise + void commands.executeCommand("vscode.openFolder", Uri.file(projectFolder), { + forceNewWindow: (workspace.workspaceFolders?.length ?? 0) > 0, + }); + } + + private async _update(): Promise { + this._panel.title = "New Rust Pico Project"; + // TODO: setup latest SDK and Toolchain VB before creating the project + this._panel.iconPath = Uri.joinPath( + this._extensionUri, + "web", + "raspberry-128.png" + ); + const html = this._getHtmlForWebview(this._panel.webview); + + if (html !== "") { + try { + this._panel.webview.html = html; + } catch (error) { + this._logger.error( + "Failed to set webview html. Webview might have been disposed. Error: ", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + + return; + } + await this._updateTheme(); + } else { + void window.showErrorMessage( + "Failed to load webview for new Rust Pico project" + ); + this.dispose(); + } + } + + private async _updateTheme(): Promise { + try { + await this._panel.webview.postMessage({ + command: "setTheme", + theme: + window.activeColorTheme.kind === ColorThemeKind.Dark || + window.activeColorTheme.kind === ColorThemeKind.HighContrast + ? "dark" + : "light", + }); + } catch (error) { + this._logger.error( + "Failed to update theme in webview. Webview might have been disposed. Error:", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + } + } + + public dispose(): void { + NewRustProjectPanel.currentPanel = undefined; + + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + + if (x) { + x.dispose(); + } + } + } + + private _getHtmlForWebview(webview: Webview): string { + const mainScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "rust", "main.js") + ); + + const mainStyleUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "main.css") + ); + + const tailwindcssScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "tailwindcss-3_3_5.js") + ); + + // images + const navHeaderSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header.svg") + ); + + const navHeaderDarkSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header-dark.svg") + ); + + // Restrict the webview to only load specific scripts + const nonce = getNonce(); + + return ` + + + + + + + + + + New Pico Rust Project + + + + + +
+
+ + +
+
+

Basic Settings

+
+
+ +
+
+ + +
+
+ + +
+ +

+ Note: The Pico extension will always install and use the latest stable version of Rust. +

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

Flash Method

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + + + `; + } +} diff --git a/web/mpy/main.js b/web/mpy/main.js index 56331315..5331e034 100644 --- a/web/mpy/main.js +++ b/web/mpy/main.js @@ -173,7 +173,6 @@ var submitted = false; document.getElementById('inp-project-name').addEventListener('input', function () { const projName = document.getElementById('inp-project-name').value; - console.log(`${projName} is now`); // TODO: future examples stuff (maybe) }); diff --git a/web/rust/main.js b/web/rust/main.js new file mode 100644 index 00000000..913a3dc7 --- /dev/null +++ b/web/rust/main.js @@ -0,0 +1,146 @@ +"use strict"; + +const CMD_CHANGE_LOCATION = 'changeLocation'; +const CMD_SUBMIT = 'submit'; +const CMD_CANCEL = 'cancel'; +const CMD_SET_THEME = 'setTheme'; +const CMD_ERROR = 'error'; +const CMD_SUBMIT_DENIED = 'submitDenied'; + +var submitted = false; + +(function () { + const vscode = acquireVsCodeApi(); + + // needed so a element isn't hidden behind the navbar on scroll + const navbarOffsetHeight = document.getElementById('top-navbar').offsetHeight; + + // returns true if project name input is valid + function projectNameFormValidation(projectNameElement) { + if (typeof examples !== 'undefined') { + return true; + } + + const projectNameError = document.getElementById('inp-project-name-error'); + const projectName = projectNameElement.value; + + var invalidChars = /[\/:*?"<>| ]/; + // check for reserved names in Windows + var reservedNames = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i; + if (projectName.trim().length == 0 || invalidChars.test(projectName) || reservedNames.test(projectName)) { + projectNameError.hidden = false; + //projectNameElement.scrollIntoView({ behavior: "smooth" }); + window.scrollTo({ + top: projectNameElement.offsetTop - navbarOffsetHeight, + behavior: 'smooth' + }); + + return false; + } + + projectNameError.hidden = true; + return true; + } + + window.changeLocation = () => { + // Send a message back to the extension + vscode.postMessage({ + command: CMD_CHANGE_LOCATION, + value: null + }); + } + + window.cancelBtnClick = () => { + // close webview + vscode.postMessage({ + command: CMD_CANCEL, + value: null + }); + } + + window.submitBtnClick = () => { + /* Catch silly users who spam the submit button */ + if (submitted) { + console.error("already submitted"); + return; + } + submitted = true; + + // get all values of inputs + const projectNameElement = document.getElementById('inp-project-name'); + // if is project import then the project name element will not be rendered and does not exist in the DOM + const projectName = projectNameElement.value; + if (projectName !== undefined && !projectNameFormValidation(projectNameElement)) { + submitted = false; + return; + } + + // flash-method selection + const flashMethodRadio = document.getElementsByName('flash-method-radio'); + let flashMethodSelection = null; + for (let i = 0; i < flashMethodRadio.length; i++) { + if (flashMethodRadio[i].checked) { + flashMethodSelection = parseInt(flashMethodRadio[i].value); + break; + } + } + // if flash-method selection is null or not a number, smaller than 0 or bigger than 2, set it to 0 + if (flashMethodSelection === null || isNaN(flashMethodSelection) || flashMethodSelection < 0 || flashMethodSelection > 2) { + flashMethodSelection = 0; + console.debug('Invalid flash-method selection value: ' + flashMethodSelection); + vscode.postMessage({ + command: CMD_ERROR, + value: "Please select a valid flash-method." + }); + submitted = false; + return; + } + + //post all data values to the extension + vscode.postMessage({ + command: CMD_SUBMIT, + value: { + projectName: projectName, + flashMethod: flashMethodSelection + } + }); + } + + function _onMessage(event) { + // JSON data sent from the extension + const message = event.data; + + switch (message.command) { + case CMD_CHANGE_LOCATION: + // update UI + document.getElementById('inp-project-location').value = message.value; + break; + case CMD_SET_THEME: + console.log("set theme", message.theme); + // update UI + if (message.theme == "dark") { + // explicitly choose dark mode + localStorage.theme = 'dark' + document.body.classList.add('dark') + } else if (message.theme == "light") { + document.body.classList.remove('dark') + // explicitly choose light mode + localStorage.theme = 'light' + } + break; + case CMD_SUBMIT_DENIED: + submitted = false; + break; + default: + console.error('Unknown command: ' + message.command); + break; + } + } + + window.addEventListener("message", _onMessage); + + // add onclick event handlers to avoid inline handlers + document.getElementById('btn-change-project-location').addEventListener('click', changeLocation); + document.getElementById('btn-cancel').addEventListener('click', cancelBtnClick); + document.getElementById('btn-create').addEventListener('click', submitBtnClick); +}());