diff --git a/package.json b/package.json index a9a5c1d..1ba934d 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,12 @@ "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", diff --git a/src/commands/getPaths.mts b/src/commands/getPaths.mts index 35696cf..0e29102 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,9 +7,11 @@ import { cmakeGetPicoVar, } from "../utils/cmakeUtil.mjs"; import { join } from "path"; +import { join as joinPosix } from "path/posix"; import { buildOpenOCDPath, buildPicotoolPath, + buildSDKPath, buildToolchainPath, downloadAndInstallOpenOCD, downloadAndInstallPicotool, @@ -19,6 +21,11 @@ import which from "which"; import { execSync } from "child_process"; import { getPicotoolReleases } from "../utils/githubREST.mjs"; import { openOCDVersion } from "../webview/newProjectPanel.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"; export class GetPythonPathCommand extends CommandWithResult { constructor() { @@ -59,7 +66,7 @@ export class GetEnvPathCommand extends CommandWithResult { } export class GetGDBPathCommand extends CommandWithResult { - constructor() { + constructor(private readonly _extensionUri: Uri) { super("getGDBPath"); } @@ -72,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")) { @@ -154,6 +196,8 @@ export class GetCompilerPathCommand extends CommandWithResult { } export class GetChipCommand extends CommandWithResult { + private readonly _logger = new Logger("GetChipCommand"); + constructor() { super("getChip"); } @@ -167,6 +211,19 @@ 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 ""; + } + + return chip; + } const settings = Settings.getInstance(); let buildDir = join(workspaceFolder.uri.fsPath, "build"); @@ -227,6 +284,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"); @@ -343,3 +407,50 @@ export class GetOpenOCDRootCommand extends CommandWithResult< return buildOpenOCDPath(openOCDVersion); } } + +/** + * 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 bb0cf37..bcc4334 100644 --- a/src/commands/launchTargetPath.mts +++ b/src/commands/launchTargetPath.mts @@ -6,6 +6,7 @@ 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"; @@ -81,10 +82,20 @@ export default class LaunchTargetPathCommand extends CommandWithResult { | 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-eabi" + : "riscv32imac-unknown-none-elf"; + return joinPosix( workspace.workspaceFolders[0].uri.fsPath.replaceAll("\\", "/"), "target", - "thumbv6m-none-eabi", + toolchain, "debug", cargoToml.package.name ); diff --git a/src/commands/switchBoard.mts b/src/commands/switchBoard.mts index 99fdfd4..48b86a9 100644 --- a/src/commands/switchBoard.mts +++ b/src/commands/switchBoard.mts @@ -7,7 +7,7 @@ import { workspace, type Uri, } from "vscode"; -import { existsSync, readdirSync, readFileSync } from "fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs"; import { buildSDKPath, downloadAndInstallToolchain, @@ -25,9 +25,6 @@ import { getSupportedToolchains } from "../utils/toolchainUtil.mjs"; import VersionBundlesLoader from "../utils/versionBundles.mjs"; import State from "../state.mjs"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; -import { writeFile } from "fs/promises"; -import { parse as parseToml } from "toml"; -import { writeTomlFile } from "../utils/projectGeneration/tomlUtil.mjs"; export default class SwitchBoardCommand extends Command { private _logger: Logger = new Logger("SwitchBoardCommand"); @@ -93,20 +90,20 @@ export default class SwitchBoardCommand extends Command { // check it has a CMakeLists.txt if ( workspaceFolder === undefined || - !existsSync(join(workspaceFolder.uri.fsPath, "CMakeLists.txt")) || - isRustProject + (!existsSync(join(workspaceFolder.uri.fsPath, "CMakeLists.txt")) && + !isRustProject) ) { return; } if (isRustProject) { const board = await window.showQuickPick( - ["rp2040", "rp2350", "rp2350-RISCV"], + ["RP2040", "RP2350", "RP2350-RISCV"], { placeHolder: "Select chip", canPickMany: false, ignoreFocusOut: false, - title: "Select chip", + title: "Switch project target chip", } ); @@ -114,87 +111,37 @@ export default class SwitchBoardCommand extends Command { return undefined; } - const target = - board === "rp2350-RISCV" - ? "riscv32imac-unknown-none-elf" - : board === "rp2350" - ? "thumbv8m.main-none-eabihf" - : "thumbv6m-none-eabi"; - - // check if .cargo/config.toml already contains a line starting with - // target = "${target}" and if no replace target = "..." with it with the new target - try { - const cargoConfigPath = join( - workspaceFolder.uri.fsPath, - ".cargo", - "config.toml" - ); - - const contents = readFileSync(cargoConfigPath, "utf-8"); - - const newContents = contents.replace( - /target = ".*"/, - `target = "${target}"` + writeFileSync( + join(workspaceFolder.uri.fsPath, ".pico-rs"), + board.toLowerCase(), + "utf8" ); - - if (newContents === contents) { - return; - } - - // write new contents to file - await writeFile(cargoConfigPath, newContents); - - const cargoToml = (await parseToml(contents)) as { - features?: { default?: string[] }; - }; - - let features = cargoToml.features?.default ?? []; - - switch (board) { - case "rp2040": - features.push("rp2040"); - // remove all other features rp2350 and rp2350-riscv - features = features.filter( - f => f !== "rp2350" && f !== "rp2350-riscv" - ); - break; - case "rp2350": - features.push("rp2350"); - // remove all other features rp2040 and rp2350-riscv - features = features.filter( - f => f !== "rp2040" && f !== "rp2350-riscv" - ); - break; - case "rp2350-RISCV": - features.push("rp2350-riscv"); - // remove all other features rp2040 and rp2350 - features = features.filter(f => f !== "rp2040" && f !== "rp2350"); - break; - } - - if (cargoToml.features) { - cargoToml.features.default = features; - } else { - // not necessary becuase your project is broken at this point - cargoToml.features = { default: features }; - } - - await writeTomlFile(cargoConfigPath, cargoToml); } catch (error) { this._logger.error( - "Failed to update .cargo/config.toml", - unknownErrorToString(error) + `Failed to write .pico-rs file: ${unknownErrorToString(error)}` ); void window.showErrorMessage( - "Failed to update Cargo.toml and " + - ".cargo/config.toml - cannot update chip" + "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-eabi" + : "riscv32imac-unknown-none-elf"; + + await workspace + .getConfiguration("rust-analyzer") + .update("cargo.target", toolchain, null); + return; } diff --git a/src/extension.mts b/src/extension.mts index 73076f9..d58a6fa 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -44,6 +44,7 @@ import { GetChipUppercaseCommand, GetPicotoolPathCommand, GetOpenOCDRootCommand, + GetSVDPathCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -53,6 +54,7 @@ import { downloadAndInstallTools, downloadAndInstallPicotool, downloadAndInstallOpenOCD, + installLatestRustRequirements, } from "./utils/download.mjs"; import { SDK_REPOSITORY_URL } from "./utils/githubREST.mjs"; import { getSupportedToolchains } from "./utils/toolchainUtil.mjs"; @@ -80,8 +82,8 @@ import { NewMicroPythonProjectPanel } from "./webview/newMicroPythonProjectPanel import type { Progress as GotProgress } from "got"; import findPython, { showPythonNotFoundError } from "./utils/pythonHelper.mjs"; import { - chipFromCargoToml, downloadAndInstallRust, + rustProjectGetSelectedChip, } from "./utils/rustUtil.mjs"; import State from "./state.mjs"; import { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; @@ -114,13 +116,14 @@ export async function activate(context: ExtensionContext): Promise { new LaunchTargetPathReleaseCommand(), new GetPythonPathCommand(), new GetEnvPathCommand(), - new GetGDBPathCommand(), + new GetGDBPathCommand(context.extensionUri), new GetCompilerPathCommand(), new GetChipCommand(), new GetChipUppercaseCommand(), new GetTargetCommand(), new GetPicotoolPathCommand(), new GetOpenOCDRootCommand(), + new GetSVDPathCommand(context.extensionUri), new CompileProjectCommand(), new RunProjectCommand(), new FlashProjectSWDCommand(), @@ -288,11 +291,17 @@ export async function activate(context: ExtensionContext): Promise { return; } + const result = await installLatestRustRequirements(context.extensionUri); + + if (!result) { + return; + } + ui.showStatusBarItems(isRustProject); - const chip = await chipFromCargoToml(); + const chip = rustProjectGetSelectedChip(workspaceFolder.uri.fsPath); if (chip !== null) { - ui.updateBoard(chip); + ui.updateBoard(chip.toUpperCase()); } else { ui.updateBoard("N/A"); } diff --git a/src/utils/download.mts b/src/utils/download.mts index 46444b1..5d134be 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -14,13 +14,16 @@ 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, getGit } from "./gitUtil.mjs"; import { checkForGit } from "./requirementsUtil.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, @@ -47,6 +50,8 @@ import { WINDOWS_ARM64_PYTHON_DOWNLOAD_URL, WINDOWS_X86_PYTHON_DOWNLOAD_URL, } from "./sharedConstants.mjs"; +import VersionBundlesLoader from "./versionBundles.mjs"; +import { openOCDVersion } from "../webview/newProjectPanel.mjs"; /// Translate nodejs platform names to ninja platform names const NINJA_PLATFORMS: { [key: string]: string } = { @@ -1276,3 +1281,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(openOCDVersion, (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/projectGeneration/projectRust.mts b/src/utils/projectGeneration/projectRust.mts index 7e23d48..c2bac99 100644 --- a/src/utils/projectGeneration/projectRust.mts +++ b/src/utils/projectGeneration/projectRust.mts @@ -10,18 +10,14 @@ import { exec } from "child_process"; import { GetOpenOCDRootCommand, GetPicotoolPathCommand, + GetSVDPathCommand, } from "../../commands/getPaths.mjs"; import { extensionName } from "../../commands/command.mjs"; import { commands, window } from "vscode"; -import { getSDKReleases, SDK_REPOSITORY_URL } from "../githubREST.mjs"; -import { downloadAndInstallSDK } from "../download.mjs"; const execAsync = promisify(exec); -async function generateVSCodeConfig( - projectRoot: string, - picoSDKVersion: string -): Promise { +async function generateVSCodeConfig(projectRoot: string): Promise { const vsc = join(projectRoot, ".vscode"); // create extensions.json @@ -29,7 +25,7 @@ async function generateVSCodeConfig( recommendations: [ "marus25.cortex-debug", "rust-lang.rust-analyzer", - "probe-rs.probe-rs-debugger", + //"probe-rs.probe-rs-debugger", "raspberry-pi.raspberry-pi-pico", ], }; @@ -63,8 +59,8 @@ async function generateVSCodeConfig( "interface/cmsis-dap.cfg", "target/${command:raspberry-pi-pico.getTarget}.cfg", ], - svdFile: `\${userHome}/.pico-sdk/sdk/${picoSDKVersion}/src/\${command:raspberry-pi-pico.getChip}/hardware_regs/\${command:raspberry-pi-pico.getChipUppercase}.svd`, - runToEntryPoint: "main", + svdFile: `\${command:${extensionName}.${GetSVDPathCommand.id}}`, + runToEntryPoint: "Reset", // Fix for no_flash binaries, where monitor reset halt doesn't do what is expected // Also works fine for flash binaries overrideLaunchCommands: [ @@ -72,12 +68,31 @@ async function generateVSCodeConfig( 'load "${command:raspberry-pi-pico.launchTargetPath}"', ], openOCDLaunchCommands: ["adapter speed 5000"], + preLaunchTask: "Compile Project (debug)", + // TODO: does currently not work + rttConfig: { + enabled: true, + clearSearch: true, + address: "0x2003fbc0", + searchId: "SEGGER RTT", + searchSize: 48, + decoders: [ + { + type: "console", + encoding: "unsigned", + inputmode: "disabled", + noprompt: true, + label: "RP2", + port: 0, + }, + ], + }, }, ], }; const settings = { - "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.cargo.target": "thumbv8m.main-none-eabihf", "rust-analyzer.checkOnSave.allTargets": false, "editor.formatOnSave": true, "files.exclude": { @@ -109,6 +124,27 @@ async function generateVSCodeConfig( }, }, }, + { + 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}}`, + }, + }, + }, { label: "Run Project", type: "process", @@ -197,18 +233,18 @@ use defmt::*; use defmt_rtt as _; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; -#[cfg(feature = "rp2350")] +#[cfg(target_arch = "riscv32")] use panic_halt as _; -#[cfg(feature = "rp2040")] +#[cfg(target_arch = "arm")] use panic_probe as _; // Alias for our HAL crate use hal::entry; -#[cfg(feature = "rp2350")] +#[cfg(rp2350)] use rp235x_hal as hal; -#[cfg(feature = "rp2040")] +#[cfg(rp2040)] use rp2040_hal as hal; // use bsp::entry; @@ -221,19 +257,18 @@ use rp2040_hal as hal; /// as the BSPs already perform this step. #[link_section = ".boot2"] #[used] -#[cfg(feature = "rp2040")] +#[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\` +// \`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(feature = "rp2350")] +#[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. @@ -268,10 +303,10 @@ fn main() -> ! { ) .unwrap(); - #[cfg(feature = "rp2040")] + #[cfg(rp2040)] let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); - #[cfg(feature = "rp2350")] + #[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 @@ -348,6 +383,7 @@ interface CargoTomlDependencies { | string | TomlInlineObject<{ optional?: boolean; + path?: string; version: string; features?: string[]; }>; @@ -361,6 +397,7 @@ interface CargoToml { license: string; }; + "build-dependencies": CargoTomlDependencies; dependencies: CargoTomlDependencies; /* @@ -379,13 +416,18 @@ interface CargoToml { "'cfg( target_arch = \"riscv32\" )'": { dependencies: CargoTomlDependencies; }; - }; - features: { - default?: string[]; - rp2040: string[]; - rp2350: string[]; - "rp2350-riscv": string[]; + "thumbv6m-none-eabi": { + dependencies: CargoTomlDependencies; + }; + + "riscv32imac-unknown-none-elf": { + dependencies: CargoTomlDependencies; + }; + + '"thumbv8m.main-none-eabihf"': { + dependencies: CargoTomlDependencies; + }; }; } @@ -400,28 +442,15 @@ async function generateCargoToml( 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", - "rp235x-hal": new TomlInlineObject({ - optional: true, - path: "./rp-hal/rp235x-hal", - version: "0.2.0", - features: ["rt", "critical-section-impl"], - }), - "rp2040-hal": new TomlInlineObject({ - optional: true, - path: "./rp-hal/rp2040-hal", - version: "0.10", - features: ["rt", "critical-section-impl"], - }), - "rp2040-boot2": new TomlInlineObject({ - optional: true, - version: "0.2", - }), }, target: { "'cfg( target_arch = \"arm\" )'": { @@ -439,13 +468,36 @@ async function generateCargoToml( }), }, }, - }, + "thumbv6m-none-eabi": { + dependencies: { + "rp2040-hal": new TomlInlineObject({ + path: "./rp-hal/rp2040-hal", + version: "0.10", + features: ["rt", "critical-section-impl"], + }), + "rp2040-boot2": "0.2", + }, + }, + + "riscv32imac-unknown-none-elf": { + dependencies: { + "rp235x-hal": new TomlInlineObject({ + path: "./rp-hal/rp235x-hal", + version: "0.2", + features: ["rt", "critical-section-impl"], + }), + }, + }, - features: { - default: ["rp2040"], - rp2040: ["rp2040-hal", "rp2040-boot2"], - rp2350: ["rp235x-hal"], - "rp2350-riscv": ["rp2350"], + '"thumbv8m.main-none-eabihf"': { + dependencies: { + "rp235x-hal": new TomlInlineObject({ + path: "./rp-hal/rp235x-hal", + version: "0.2", + features: ["rt", "critical-section-impl"], + }), + }, + }, }, }; @@ -900,33 +952,56 @@ details."); async function generateBuildRs(projectRoot: string): Promise { const buildRs = `//! Set up linker scripts for the rp235x-hal examples -use std::fs::File; +use std::fs::{ File, read_to_string }; use std::io::Write; use std::path::PathBuf; -#[cfg(all(feature = "rp2040", feature = "rp2350"))] -compile_error!( - "\\"rp2040\\" and \\"rp2350\\" cannot be enabled at the same time - you must choose which to use" -); - -#[cfg(not(any(feature = "rp2040", feature = "rp2350")))] -compile_error!("You must enable either \\"rp2040\\" or \\"rp2350\\""); +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 - #[cfg(feature = "rp2040")] - let memory_x = include_bytes!("rp2040.x"); - #[cfg(feature = "rp2350")] - 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:rerun-if-changed=rp2040.x"); - println!("cargo:rerun-if-changed=rp2350.x"); + 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 = "thumbv8m.main-none-eabihf"; + } else { + target = "riscv32imac-unknown-none-elf"; + } + 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 @@ -936,7 +1011,8 @@ fn main() { println!("cargo:rerun-if-changed=rp2350_riscv.x"); println!("cargo:rerun-if-changed=build.rs"); -}`; +} +`; try { await writeFile(join(projectRoot, "build.rs"), buildRs); @@ -1098,9 +1174,10 @@ async function generateCargoConfig(projectRoot: string): Promise { # [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 = "thumbv6m-none-eabi" # target = "riscv32imac-unknown-none-elf" # Target specific options @@ -1208,8 +1285,7 @@ runner = "\${PICOTOOL_PATH} load -u -v -x -t elf" */ export async function generateRustProject( projectFolder: string, - projectName: string, - flashMethod: FlashMethod + projectName: string ): Promise { const picotoolPath: string | undefined = await commands.executeCommand( `${extensionName}.${GetPicotoolPathCommand.id}` @@ -1284,31 +1360,7 @@ export async function generateRustProject( return false; } - const picoSDK = await getSDKReleases(); - if (picoSDK.length === 0) { - Logger.error(LoggerSource.projectRust, "Failed to get SDK releases."); - - void window.showErrorMessage( - "Failed to get SDK releases. Please try again and check your settings." - ); - - return false; - } - result = await downloadAndInstallSDK(picoSDK[0], SDK_REPOSITORY_URL); - if (!result) { - Logger.error( - LoggerSource.projectRust, - "Failed to download and install SDK." - ); - - void window.showErrorMessage( - "Failed to download and install SDK. Please try again and check your settings." - ); - - return false; - } - - result = await generateVSCodeConfig(projectFolder, picoSDK[0]); + result = await generateVSCodeConfig(projectFolder); if (!result) { return false; } @@ -1320,10 +1372,7 @@ export async function generateRustProject( // add .pico-rs file try { - await writeFile( - join(projectFolder, ".pico-rs"), - JSON.stringify({ flashMethod }) - ); + await writeFile(join(projectFolder, ".pico-rs"), "rp2350"); } catch (error) { Logger.error( LoggerSource.projectRust, diff --git a/src/utils/projectGeneration/tomlUtil.mts b/src/utils/projectGeneration/tomlUtil.mts index 131a898..5d684ad 100644 --- a/src/utils/projectGeneration/tomlUtil.mts +++ b/src/utils/projectGeneration/tomlUtil.mts @@ -1,9 +1,7 @@ import { assert } from "console"; import { writeFile } from "fs/promises"; -// TODO: maybe not needed - -export class TomlInlineObject> { +export class TomlInlineObject> { constructor(public values: T) {} public toString(): string { return `{ ${Object.entries(this.values) @@ -18,6 +16,7 @@ export class TomlInlineObject> { "TomlInlineObject Value must not be an object." ); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${key} = ${typeof value === "string" ? `"${value}"` : value}`; }) .join(", ")} }`; @@ -39,7 +38,11 @@ function tomlObjectToString( .filter(([, value]) => value !== null && value !== undefined) // sort entries by type of value (object type last) .sort(([, value1], [, value2]) => - typeof value1 === "object" ? 1 : typeof value2 === "object" ? -1 : 0 + typeof value1 === "object" && !(value1 instanceof Array) + ? 1 + : typeof value2 === "object" && !(value2 instanceof Array) + ? -1 + : 0 ) .reduce((acc, [key, value]) => { if (value instanceof TomlInlineObject) { @@ -61,7 +64,9 @@ function tomlObjectToString( return acc; } - acc += `${acc.length > 0 ? "\n" : ""}[${parent + key}]\n`; + acc += `${acc.split("\n")[0].split(".").length <= 1 ? "\n" : ""}[${ + parent + key + }]\n`; acc += tomlObjectToString(value as object, parent + key + "."); } else { acc += `${key} = ${ diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 45fcad1..9a053c1 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -1,14 +1,11 @@ -import { homedir, tmpdir } from "os"; -import { downloadAndInstallGithubAsset } from "./download.mjs"; -import { getRustToolsReleases, GithubRepository } from "./githubREST.mjs"; -import { mkdirSync, renameSync } from "fs"; +import { readFileSync, writeFileSync } from "fs"; import Logger, { LoggerSource } from "../logger.mjs"; import { unknownErrorToString } from "./errorHelper.mjs"; -import { env, ProgressLocation, Uri, window, workspace } from "vscode"; +import { env, ProgressLocation, Uri, window } from "vscode"; import { promisify } from "util"; import { exec } from "child_process"; -import { dirname, join } from "path"; -import { parse as parseToml } from "toml"; +import { join } from "path"; +import { homedir } from "os"; /*const STABLE_INDEX_DOWNLOAD_URL = "https://static.rust-lang.org/dist/channel-rust-stable.toml";*/ @@ -202,12 +199,19 @@ async function checkHostToolchainInstalled(): Promise { export async function installHostToolchain(): Promise { try { + // TODO: maybe listen for stderr // this will automatically take care of having the correct - // recommended host toolchain installed - await execAsync("rustup show", { + // 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( @@ -324,24 +328,30 @@ export async function downloadAndInstallRust(): Promise { return false; } - // install target - const target = "thumbv6m-none-eabi"; - 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)}` - ); + // 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.` - ); + void window.showErrorMessage( + `Failed to install target '${target}'. Please check the logs.` + ); - return false; + return false; + } } // install flip-link @@ -381,18 +391,19 @@ export async function downloadAndInstallRust(): Promise { } // install cargo-generate binary - result = await installCargoGenerate(); + /*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": @@ -479,8 +490,9 @@ async function installCargoGenerate(): Promise { } return true; -} +}*/ +/* function flashMethodToArg(fm: FlashMethod): string { switch (fm) { case FlashMethod.cargoEmbed: @@ -491,6 +503,7 @@ function flashMethodToArg(fm: FlashMethod): string { } } + export async function generateRustProject( projectFolder: string, name: string, @@ -552,40 +565,39 @@ export async function generateRustProject( } return true; -} - -export async function chipFromCargoToml(): Promise { - const workspaceFolder = workspace.workspaceFolders?.[0]; +}*/ - if (!workspaceFolder) { - Logger.error(LoggerSource.rustUtil, "No workspace folder found."); - - return null; - } +export function rustProjectGetSelectedChip( + workspaceFolder: string +): "rp2040" | "rp2350" | "rp2350-riscv" | null { + const picors = join(workspaceFolder, ".pico-rs"); try { - const cargoTomlPath = join(workspaceFolder.uri.fsPath, "Cargo.toml"); - const contents = await workspace.fs.readFile(Uri.file(cargoTomlPath)); + const contents = readFileSync(picors, "utf-8").trim(); - const cargoToml = (await parseToml(new TextDecoder().decode(contents))) as { - features?: { default?: string[] }; - }; + if ( + contents !== "rp2040" && + contents !== "rp2350" && + contents !== "rp2350-riscv" + ) { + Logger.error( + LoggerSource.rustUtil, + `Invalid chip in .pico-rs: ${contents}` + ); - const features = cargoToml.features?.default ?? []; + // reset to rp2040 + writeFileSync(picors, "rp2040", "utf-8"); - if (features.includes("rp2040")) { return "rp2040"; - } else if (features.includes("rp2350")) { - return "rp2350"; - } else if (features.includes("rp2350-riscv")) { - return "rp2350-RISCV"; } + + return contents; } catch (error) { Logger.error( LoggerSource.rustUtil, - `Failed to read Cargo.toml: ${unknownErrorToString(error)}` + `Failed to read .pico-rs: ${unknownErrorToString(error)}` ); - } - return null; + return null; + } } diff --git a/src/utils/versionBundles.mts b/src/utils/versionBundles.mts index ae210c2..56260b8 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/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts index b7728cf..09b74a3 100644 --- a/src/webview/newRustProjectPanel.mts +++ b/src/webview/newRustProjectPanel.mts @@ -27,6 +27,7 @@ import { type FlashMethod, generateRustProject, } from "../utils/projectGeneration/projectRust.mjs"; +import { installLatestRustRequirements } from "../utils/download.mjs"; interface SubmitMessageValue { projectName: string; @@ -288,12 +289,18 @@ export class NewRustProjectPanel { return; } + let result = await installLatestRustRequirements(this._extensionUri); + + if (!result) { + return; + } + const projectFolder = join(projectPath, data.projectName); - const result = await generateRustProject( + result = await generateRustProject( projectFolder, - data.projectName, - data.flashMethod + data.projectName + //data.flashMethod ); if (!result) { @@ -317,7 +324,7 @@ export class NewRustProjectPanel { 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",