diff --git a/README.md b/README.md index 71b71db6..9bb992ae 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,141 @@ -# Raspberry Pi Pico Visual Studio Code extension - -> **Note: The extension is currently under development.** - -This is the official Visual Studio Code extension for Raspberry Pi Pico development. This extension equips you with a suite of tools designed to streamline your Pico projects using Visual Studio Code and the official [Pico SDK](https://github.com/raspberrypi/pico-sdk). - -For comprehensive setup instructions, refer to the [Getting Started guide](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf) PDF. - -If you have any issues while installing, please check out the [Troubleshooting](#troubleshooting) section. - -[Download latest Beta 📀](https://github.com/raspberrypi/pico-vscode/releases) - -## Features - -### Project Setup and Management - -- **Project Generator**: Easily create and configure new projects with support for advanced Pico features like I2C and PIO. The generator targets the Ninja build system and allows customization during project creation. -- **Quick Project Setup**: Initiate new Pico projects directly from the Explorer view, when no workspace is open. -- **MicroPython Support**: Create and develop MicroPython-based Pico projects with support provided through the [MicroPico](https://github.com/paulober/MicroPico) extension. - -### Configuration and Tool Management - -- **Automatic CMake Configuration**: Automatically configures CMake when loading a project. -- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. -- **No Manual Setup Required**: Automatically handles environment variables, toolchain, SDK, and tool management. -- **Includes an Uninstaller**: Easily remove the extension along with all automatically installed tools and SDKs. - -### Build, Debug, and Documentation - -- **One-Click Compilation and Debugging**: Automatically configure OpenOCD, Ninja, and CMake, allowing you to compile and debug with a single click. -- **Offline Documentation**: Conveniently access Pico SDK documentation directly within the editor, even when offline. - -- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. -- **No Manual Setup Required**: The extension handles environment variables, toolchain, SDK, and tool management for you. -- **One-Click Compilation**: Compile projects directly from the status bar with your selected SDK and tools. -- **Offline Documentation**: Access Pico SDK documentation offline. -- **Quick Project Setup**: Quickly create new Pico projects from the Explorer view when no workspace is open. -- **MicroPython Support**: Create MicroPython-based Pico projects with support provided through the MicroPico extension. - -## Requirements by OS - -> **Supported Platforms: Raspberry Pi OS (64-bit), Windows 10/11 (x86_64), macOS Sonoma (14.0) and newer, Linux x64 and arm64** - -- Visual Studio Code v1.92.1 or later - -### Raspberry Pi OS and Windows - -No additional requirements are needed. - -### macOS -To meet the requirements for macOS, run the following command in Terminal to install necessary tools: -```zsh -xcode-select --install -``` -This command installs all of the necessary tools, including but not limited to: -- **Git 2.28 or later** (ensure it's in your PATH) -- **Tar** (ensure it's in your PATH) - -### Linux -- **Python 3.9 or later** (ensure it’s in your PATH or set in settings) -- **Git 2.28 or later** (ensure it’s in your PATH) -- **Tar** (ensure it’s in your PATH) -- **\[Optional\]** gdb-multiarch for debugging (x86_64 only) -- **\[Optional\]** udev rules installed to use [OpenOCD](https://github.com/raspberrypi/openocd/blob/sdk-2.0.0/contrib/60-openocd.rules) and [picotool](https://github.com/raspberrypi/picotool/blob/master/udev/99-picotool.rules) without `sudo`, for debugging and loading -- For **\[Ubuntu 22.04\]**, install `libftdi1-2` and `libhidapi-hidraw0` packages to use OpenOCD - -## Extension Settings - -This extension provides the following settings: - -* `raspberry-pi-pico.cmakePath`: Specify a custom path for CMake. -* `raspberry-pi-pico.python3Path`: Specify a custom path for Python 3 _(machine scoped)_. -* `raspberry-pi-pico.ninjaPath`: Specify a custom path for Ninja. -* `raspberry-pi-pico.gitPath`: Specify a custom path for Git. -* `raspberry-pi-pico.cmakeAutoConfigure`: Enable/Disable automatic CMake configuration when project is opened -* `raspberry-pi-pico.useCmakeTools`: Enable/Disable the CMake Tools Extension Integration (see below) -* `raspberry-pi-pico.githubToken`: Provide a GitHub personal access token (classic) with the `public_repo` scope. This token is used to check for available versions of the Pico SDK and other tools. Without it, the extension uses the unauthenticated GitHub API, which has a lower rate limit and may lead to restricted functionality if the limit is exceeded. The unauthenticated rate limit is per public IP address, so a token is more necessary if your IP is shared with many users. - -## CMake Tools Extension Integration - -For more complex projects, such as those with multiple executables or when the project name is defined as a variable, this extension can integrate with the CMake Tools extension to enhance CMake parsing. You can enable the CMake Tools Extension integration during project generation, using the checkbox at the bottom of the page. To enable it for an existing project, just re-import the project with the option selected. Alternatively, to manually enable it, adjust the following settings in your `settings.json`: - -- `raspberry-pi-pico.cmakeAutoConfigure`: Set from `true` to `false`. -- `raspberry-pi-pico.useCmakeTools`: Set from `false` to `true`. - -For optimal functionality, consider enabling: - -- `cmake.configureOnEdit`: true -- `cmake.automaticReconfigure`: true -- `cmake.configureOnOpen`: true - -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. - -## 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: - -1. Download the sample profile from [here](scripts/Pico.code-profile). -2. Open Command Palette with `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) and select `Profiles: Import Profile`. -3. Import the downloaded file to install the extension in a dedicated Pico profile. -4. This setup helps isolate the Pico extension from other extensions, reducing the risk of conflicts. - -## Troubleshooting - -If you're having issues with installation, this is usually due to a download failure. To retry downloading everything, you can: -- Clear all setting for the extension (`Ctrl+Shift+P` -> `Preferences: Open Settings (UI)` -> Search for `raspberry-pi-pico` and reset everything to default) -- Uninstall the SDK (`Ctrl+Shift+P` -> `Raspberry Pi Pico: Uninstall Pico SDK`) -- Uninstall the extension, close & reopen VS Code, then reinstall the extension - -Also make sure you've deleted any previous SDK installations (eg Pico setup for Windows, or a manual installation you've done), as those can conflict with this extension. - -If you're still unable to get it working, then [file a bug report](https://github.com/raspberrypi/pico-vscode/issues/new?template=bug_report.md) and **fill out all the fields** so we can figure out the problem. - -## Known Issues - -- Custom Paths: Custom paths for Ninja, Python3, and Git are not stored in `CMakeLists.txt` like SDK and Toolchain paths. You need to build and configure projects through the extension to use these custom paths. - -### GitHub API Rate Limit ("Error while retrieving SDK and toolchain versions") - -If you encounter issues retrieving available Pico SDK versions, it may be due to GitHub API rate limits. To resolve this, create a personal access token (classic PAT) with the `public_repo` scope and set it in the global (User) extension settings to increase your rate limit. - -## Build Instructions - -For advanced users who want to build the extension `.vsix` file, follow these steps: - -1. Install nodejs ([Instructions Windows](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows)) -2. Update npm: `npm install -g npm` -3. Install VSCE globally: `npm install -g @vscode/vsce` -4. Run `npm ci` in the project directory to install dependencies. -5. Build the extension with: `vsce package` - -This will generate a `.vsix` file, which you can install in VS Code using `code --install-extension path-to.vsix` or via the GUI: `Extensions > three dots > Install from VSIX`. +# Raspberry Pi Pico Visual Studio Code extension + +> **Note: The extension is currently under development.** + +This is the official Visual Studio Code extension for Raspberry Pi Pico development. This extension equips you with a suite of tools designed to streamline your Pico projects using Visual Studio Code and the official [Pico SDK](https://github.com/raspberrypi/pico-sdk). + +For comprehensive setup instructions, refer to the [Getting Started guide](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf) PDF. + +If you have any issues while installing, please check out the [Troubleshooting](#troubleshooting) section. + +[Download latest Beta 📀](https://github.com/raspberrypi/pico-vscode/releases) + +## Features + +### Project Setup and Management + +- **Project Generator**: Easily create and configure new projects with support for advanced Pico features like I2C and PIO. The generator targets the Ninja build system and allows customization during project creation. +- **Quick Project Setup**: Initiate new Pico projects directly from the Explorer view, when no workspace is open. +- **MicroPython Support**: Create and develop MicroPython-based Pico projects with support provided through the [MicroPico](https://github.com/paulober/MicroPico) extension. + +### Configuration and Tool Management + +- **Automatic CMake Configuration**: Automatically configures CMake when loading a project. +- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. +- **No Manual Setup Required**: Automatically handles environment variables, toolchain, SDK, and tool management. +- **Includes an Uninstaller**: Easily remove the extension along with all automatically installed tools and SDKs. + +### Build, Debug, and Documentation + +- **One-Click Compilation and Debugging**: Automatically configure OpenOCD, Ninja, and CMake, allowing you to compile and debug with a single click. +- **Offline Documentation**: Conveniently access Pico SDK documentation directly within the editor, even when offline. + +- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. +- **No Manual Setup Required**: The extension handles environment variables, toolchain, SDK, and tool management for you. +- **One-Click Compilation**: Compile projects directly from the status bar with your selected SDK and tools. +- **Offline Documentation**: Access Pico SDK documentation offline. +- **Quick Project Setup**: Quickly create new Pico projects from the Explorer view when no workspace is open. +- **MicroPython Support**: Create MicroPython-based Pico projects with support provided through the MicroPico extension. + +## Requirements by OS + +> **Supported Platforms: Raspberry Pi OS (64-bit), Windows 10/11 (x86_64), macOS Sonoma (14.0) and newer, Linux x64 and arm64** + +- Visual Studio Code v1.92.1 or later + +### Raspberry Pi OS and Windows + +No additional requirements are needed. + +### macOS +To meet the requirements for macOS, run the following command in Terminal to install necessary tools: +```zsh +xcode-select --install +``` +This command installs all of the necessary tools, including but not limited to: +- **Git 2.28 or later** (ensure it's in your PATH) +- **Tar** (ensure it's in your PATH) + +### Linux +- **Python 3.9 or later** (ensure it’s in your PATH or set in settings) +- **Git 2.28 or later** (ensure it’s in your PATH) +- **Tar** (ensure it’s in your PATH) +- **\[Optional\]** gdb-multiarch for debugging (x86_64 only) +- **\[Optional\]** udev rules installed to use [OpenOCD](https://github.com/raspberrypi/openocd/blob/sdk-2.0.0/contrib/60-openocd.rules) and [picotool](https://github.com/raspberrypi/picotool/blob/master/udev/99-picotool.rules) without `sudo`, for debugging and loading +- For **\[Ubuntu 22.04\]**, install `libftdi1-2` and `libhidapi-hidraw0` packages to use OpenOCD + +## Extension Settings + +This extension provides the following settings: + +* `raspberry-pi-pico.cmakePath`: Specify a custom path for CMake. +* `raspberry-pi-pico.python3Path`: Specify a custom path for Python 3 _(machine scoped)_. +* `raspberry-pi-pico.ninjaPath`: Specify a custom path for Ninja. +* `raspberry-pi-pico.gitPath`: Specify a custom path for Git. +* `raspberry-pi-pico.cmakeAutoConfigure`: Enable/Disable automatic CMake configuration when project is opened +* `raspberry-pi-pico.useCmakeTools`: Enable/Disable the CMake Tools Extension Integration (see below) +* `raspberry-pi-pico.githubToken`: Provide a GitHub personal access token (classic) with the `public_repo` scope. This token is used to check for available versions of the Pico SDK and other tools. Without it, the extension uses the unauthenticated GitHub API, which has a lower rate limit and may lead to restricted functionality if the limit is exceeded. The unauthenticated rate limit is per public IP address, so a token is more necessary if your IP is shared with many users. + +## CMake Tools Extension Integration + +For more complex projects, such as those with multiple executables or when the project name is defined as a variable, this extension can integrate with the CMake Tools extension to enhance CMake parsing. You can enable the CMake Tools Extension integration during project generation, using the checkbox at the bottom of the page. To enable it for an existing project, just re-import the project with the option selected. Alternatively, to manually enable it, adjust the following settings in your `settings.json`: + +- `raspberry-pi-pico.cmakeAutoConfigure`: Set from `true` to `false`. +- `raspberry-pi-pico.useCmakeTools`: Set from `false` to `true`. + +For optimal functionality, consider enabling: + +- `cmake.configureOnEdit`: true +- `cmake.automaticReconfigure`: true +- `cmake.configureOnOpen`: true + +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: + +1. Download the sample profile from [here](scripts/Pico.code-profile). +2. Open Command Palette with `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) and select `Profiles: Import Profile`. +3. Import the downloaded file to install the extension in a dedicated Pico profile. +4. This setup helps isolate the Pico extension from other extensions, reducing the risk of conflicts. + +## Troubleshooting + +If you're having issues with installation, this is usually due to a download failure. To retry downloading everything, you can: +- Clear all setting for the extension (`Ctrl+Shift+P` -> `Preferences: Open Settings (UI)` -> Search for `raspberry-pi-pico` and reset everything to default) +- Uninstall the SDK (`Ctrl+Shift+P` -> `Raspberry Pi Pico: Uninstall Pico SDK`) +- Uninstall the extension, close & reopen VS Code, then reinstall the extension + +Also make sure you've deleted any previous SDK installations (eg Pico setup for Windows, or a manual installation you've done), as those can conflict with this extension. + +If you're still unable to get it working, then [file a bug report](https://github.com/raspberrypi/pico-vscode/issues/new?template=bug_report.md) and **fill out all the fields** so we can figure out the problem. + +## Known Issues + +- Custom Paths: Custom paths for Ninja, Python3, and Git are not stored in `CMakeLists.txt` like SDK and Toolchain paths. You need to build and configure projects through the extension to use these custom paths. + +### GitHub API Rate Limit ("Error while retrieving SDK and toolchain versions") + +If you encounter issues retrieving available Pico SDK versions, it may be due to GitHub API rate limits. To resolve this, create a personal access token (classic PAT) with the `public_repo` scope and set it in the global (User) extension settings to increase your rate limit. + +## Build Instructions + +For advanced users who want to build the extension `.vsix` file, follow these steps: + +1. Install nodejs ([Instructions Windows](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows)) +2. Update npm: `npm install -g npm` +3. Install VSCE globally: `npm install -g @vscode/vsce` +4. Run `npm ci` in the project directory to install dependencies. +5. Build the extension with: `vsce package` + +This will generate a `.vsix` file, which you can install in VS Code using `code --install-extension path-to.vsix` or via the GUI: `Extensions > three dots > Install from VSIX`. diff --git a/package-lock.json b/package-lock.json index 24865033..c443931c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "raspberry-pi-pico", - "version": "0.17.5", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "raspberry-pi-pico", - "version": "0.17.5", + "version": "0.18.0", "cpu": [ "x64", "arm64" @@ -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" @@ -95,9 +96,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -110,9 +111,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -120,9 +121,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -170,13 +171,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -190,13 +194,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -350,28 +354,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", - "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1059,9 +1041,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1136,24 +1118,10 @@ "vscode": "^1.78.0" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1239,31 +1207,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1291,16 +1238,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -1328,37 +1265,6 @@ "node": ">=18" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1425,63 +1331,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1588,100 +1437,18 @@ "node": ">=10" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1696,24 +1463,23 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -1721,9 +1487,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1737,8 +1503,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -1772,9 +1537,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1789,9 +1554,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1802,15 +1567,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1872,98 +1637,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", - "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2066,24 +1739,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2147,26 +1802,6 @@ "node": ">= 18" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2192,45 +1827,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -2284,9 +1880,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2320,19 +1916,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/got": { "version": "14.4.7", "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", @@ -2375,19 +1958,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2407,23 +1977,6 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "license": "BSD-2-Clause" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -2437,19 +1990,6 @@ "node": ">=10.19.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2487,13 +2027,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ini": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", @@ -2503,16 +2036,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2578,13 +2101,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -2741,39 +2257,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2811,29 +2294,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -2882,16 +2342,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -2904,52 +2354,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3028,16 +2432,6 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3080,16 +2474,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -3103,16 +2487,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3123,20 +2497,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3147,22 +2507,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3206,32 +2550,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3354,23 +2672,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3416,13 +2717,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -3436,29 +2730,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -3469,29 +2740,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3513,82 +2761,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3629,16 +2801,6 @@ "source-map": "^0.6.0" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3806,15 +2968,11 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "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", @@ -3861,21 +3019,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3929,16 +3072,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3962,16 +3095,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -4085,13 +3208,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4104,26 +3220,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index 6d309897..1f35bcf2 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", @@ -217,13 +236,31 @@ "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" + }, + { + "command": "raspberry-pi-pico.sbomTargetPathDebug", + "title": "Get path of the project debug SBOM (rust only)", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, + { + "command": "raspberry-pi-pico.sbomTargetPathRelease", + "title": "Get path of the project release SBOM (rust only)", + "category": "Raspberry Pi Pico", + "enablement": "false" } ], "configuration": { @@ -331,6 +368,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 988e8d34..9940097e 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -3,7 +3,9 @@ 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"; import { cmakeToolsForcePicoKit } from "../utils/cmakeToolsUtil.mjs"; + export default class CompileProjectCommand extends CommandWithResult { private _logger: Logger = new Logger("CompileProjectCommand"); @@ -14,13 +16,18 @@ export default class CompileProjectCommand extends CommandWithResult { } async execute(): Promise { + const isRustProject = State.getInstance().isRustProject; + // Get the task with the specified name const task = (await tasks.fetchTasks()).find( - task => task.name === "Compile Project" + task => + task.name === + (isRustProject ? "Build + Generate SBOM (release)" : "Compile Project") ); 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..c41cd91b 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[1].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[1].riscvToolchain + : latestVb[1].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,84 @@ 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; + } + + const theChip = chip === "rp2350-riscv" ? "rp2350" : chip; + + return joinPosix( + buildSDKPath(latestSDK), + "src", + theChip, + "hardware_regs", + `${theChip.toUpperCase()}.svd` + ); + } +} diff --git a/src/commands/launchTargetPath.mts b/src/commands/launchTargetPath.mts index 6cc83e71..958f0bcf 100644 --- a/src/commands/launchTargetPath.mts +++ b/src/commands/launchTargetPath.mts @@ -3,106 +3,208 @@ 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 { cmakeToolsForcePicoKit } from "../utils/cmakeToolsUtil.mjs"; +import { rustProjectGetSelectedChip } from "../utils/rustUtil.mjs"; -export default class LaunchTargetPathCommand extends CommandWithResult { - constructor() { - super("launchTargetPath"); +/* ----------------------------- Shared helpers ----------------------------- */ + +type SupportedChip = "rp2040" | "rp2350" | "rp2350-riscv" | null; + +const wsRoot = (): string => workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""; + +const norm = (p: string): string => p.replaceAll("\\", "/"); + +const isRustProject = (): boolean => State.getInstance().isRustProject; + +const chipToTriple = (chip: SupportedChip): string => { + switch (chip) { + case "rp2040": + return "thumbv6m-none-eabi"; + case "rp2350": + return "thumbv8m.main-none-eabihf"; + case "rp2350-riscv": + case null: + default: + // Default to RISC-V if unknown/null. Change if you prefer a different default. + return "riscv32imac-unknown-none-elf"; + } +}; + +const readCargoPackageName = (root: string): string | undefined => { + try { + const contents = readFileSync(join(root, "Cargo.toml"), "utf-8"); + const cargo = parseToml(contents) as + | { package?: { name?: string } } + | undefined; + + return cargo?.package?.name; + } catch { + return undefined; + } +}; + +const rustTargetPath = ( + root: string, + mode: "debug" | "release", + file?: string +): string => { + const chip: SupportedChip = rustProjectGetSelectedChip(root); + const triple = chipToTriple(chip); + + return joinPosix(norm(root), "target", triple, mode, file ?? ""); +}; + +const readProjectNameFromCMakeLists = async ( + filename: string +): Promise => { + const fileContent = readFileSync(filename, "utf-8"); + + const projectMatch = /project\(([^)\s]+)/.exec(fileContent); + if (!projectMatch?.[1]) { + return null; } - private async readProjectNameFromCMakeLists( - filename: string - ): Promise { - // Read the file - const fileContent = readFileSync(filename, "utf-8"); - - // Match the project line using a regular expression - const regex = /project\(([^)\s]+)/; - const match = regex.exec(fileContent); - - // Match for poll and threadsafe background inclusions - const regexBg = /pico_cyw43_arch_lwip_threadsafe_background/; - const matchBg = regexBg.exec(fileContent); - const regexPoll = /pico_cyw43_arch_lwip_poll/; - const matchPoll = regexPoll.exec(fileContent); - - // Extract the project name from the matched result - if (match && match[1]) { - const projectName = match[1].trim(); - - if (matchBg && matchPoll) { - // For examples with both background and poll, let user pick which to run - const quickPickItems = ["Threadsafe Background", "Poll"]; - const backgroundOrPoll = await window.showQuickPick(quickPickItems, { - placeHolder: "Select PicoW Architecture", - }); - if (backgroundOrPoll === undefined) { - return projectName; - } - - switch (backgroundOrPoll) { - case quickPickItems[0]: - return projectName + "_background"; - case quickPickItems[1]: - return projectName + "_poll"; - } - } + const projectName = projectMatch[1].trim(); - return projectName; + const hasBg = /pico_cyw43_arch_lwip_threadsafe_background/.test(fileContent); + const hasPoll = /pico_cyw43_arch_lwip_poll/.test(fileContent); + + if (hasBg && hasPoll) { + const choice = await window.showQuickPick( + ["Threadsafe Background", "Poll"], + { placeHolder: "Select PicoW Architecture" } + ); + if (choice === "Threadsafe Background") { + return `${projectName}_background`; } + if (choice === "Poll") { + return `${projectName}_poll`; + } + // user dismissed → fall back to base name + } - return null; // Return null if project line is not found + return projectName; +}; + +const tryCMakeToolsLaunchPath = async (): Promise => { + const settings = Settings.getInstance(); + if (settings?.getBoolean(SettingsKey.useCmakeTools)) { + await cmakeToolsForcePicoKit(); + const path: string = await commands.executeCommand( + "cmake.launchTargetPath" + ); + if (path) { + return norm(path); + } + } + + return null; +}; + +/* ------------------------------ Main command ------------------------------ */ + +export default class LaunchTargetPathCommand extends CommandWithResult { + public static readonly id = "launchTargetPath"; + constructor() { + super(LaunchTargetPathCommand.id); } async execute(): Promise { - if ( - workspace.workspaceFolders === undefined || - workspace.workspaceFolders.length === 0 - ) { + const root = wsRoot(); + if (!root) { return ""; } - const settings = Settings.getInstance(); - if ( - settings !== undefined && - settings.getBoolean(SettingsKey.useCmakeTools) - ) { - // Ensure the Pico kit is selected - await cmakeToolsForcePicoKit(); - - // Compile with CMake Tools - const path: string = await commands.executeCommand( - "cmake.launchTargetPath" - ); - if (path) { - return path.replaceAll("\\", "/"); + if (isRustProject()) { + const name = readCargoPackageName(root); + if (!name) { + return ""; } - } - const fsPathFolder = workspace.workspaceFolders[0].uri.fsPath; + return rustTargetPath(root, "debug", name); + } - const projectName = await this.readProjectNameFromCMakeLists( - join(fsPathFolder, "CMakeLists.txt") - ); + // Non-Rust: try CMake Tools first + const cmakeToolsPath = await tryCMakeToolsLaunchPath(); + if (cmakeToolsPath) { + return cmakeToolsPath; + } - if (projectName === null) { + // Fallback: parse CMakeLists + compile task, then return build/.elf + const cmakelists = join(root, "CMakeLists.txt"); + const projectName = await readProjectNameFromCMakeLists(cmakelists); + if (!projectName) { return ""; } - // Compile before returning const compiled = await commands.executeCommand( "raspberry-pi-pico.compileProject" ); - if (!compiled) { throw new Error( "Failed to compile project - check output from the Compile Project task" ); } - return join(fsPathFolder, "build", projectName + ".elf").replaceAll( - "\\", - "/" - ); + return norm(join(root, "build", `${projectName}.elf`)); + } +} + +/* -------------------------- Rust-specific commands ------------------------- */ + +export class LaunchTargetPathReleaseCommand extends CommandWithResult { + public static readonly id = "launchTargetPathRelease"; + constructor() { + super(LaunchTargetPathReleaseCommand.id); + } + + execute(): string { + const root = wsRoot(); + if (!root || !isRustProject()) { + return ""; + } + + const name = readCargoPackageName(root); + if (!name) { + return ""; + } + + return rustTargetPath(root, "release", name); + } +} + +export class SbomTargetPathDebugCommand extends CommandWithResult { + public static readonly id = "sbomTargetPathDebug"; + constructor() { + super(SbomTargetPathDebugCommand.id); + } + + execute(): string { + const root = wsRoot(); + if (!root || !isRustProject()) { + return ""; + } + + // sbom is a fixed filename living next to build artifacts + return rustTargetPath(root, "debug", "sbom.spdx.json"); + } +} + +export class SbomTargetPathReleaseCommand extends CommandWithResult { + public static readonly id = "sbomTargetPathRelease"; + constructor() { + super(SbomTargetPathReleaseCommand.id); + } + + execute(): string { + const root = wsRoot(); + if (!root || !isRustProject()) { + return ""; + } + + return rustTargetPath(root, "release", "sbom.spdx.json"); } } 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 30d4fa16..f979c1e0 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({ @@ -254,7 +308,7 @@ export default class SwitchBoardCommand extends Command { } } } - ) + ); } const success = await cmakeUpdateBoard(workspaceFolder.uri, board); diff --git a/src/commands/updateOpenOCD.mts b/src/commands/updateOpenOCD.mts index 54d44ba1..21bb56c8 100644 --- a/src/commands/updateOpenOCD.mts +++ b/src/commands/updateOpenOCD.mts @@ -5,7 +5,7 @@ import { rimraf } from "rimraf"; import { join } from "path"; import { homedir } from "os"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; -import { openOCDVersion } from "../webview/newProjectPanel.mjs"; +import { OPENOCD_VERSION } from "../utils/sharedConstants.mjs"; export default class UpdateOpenOCDCommand extends Command { private _logger: Logger = new Logger("UpdateOpenOCDCommand"); @@ -22,7 +22,7 @@ export default class UpdateOpenOCDCommand extends Command { try { // rimraf ~/.pico-sdk/openocd/$openOCDVersion - await rimraf(join(homedir(), ".pico-sdk/openocd", openOCDVersion), { + await rimraf(join(homedir(), ".pico-sdk/openocd", OPENOCD_VERSION), { preserveRoot: false, maxRetries: 1, retryDelay: 1000, 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 c3f5ac36..4b2b2a1f 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -33,7 +33,11 @@ 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, + SbomTargetPathDebugCommand, + SbomTargetPathReleaseCommand, +} from "./commands/launchTargetPath.mjs"; import { GetPythonPathCommand, GetEnvPathCommand, @@ -44,6 +48,8 @@ import { GetTargetCommand, GetChipUppercaseCommand, GetPicotoolPathCommand, + GetOpenOCDRootCommand, + GetSVDPathCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -53,13 +59,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"; @@ -83,7 +89,16 @@ 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 { cmakeToolsForcePicoKit } from "./utils/cmakeToolsUtil.mjs"; +import { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; +import { OPENOCD_VERSION } from "./utils/sharedConstants.mjs"; +import VersionBundlesLoader from "./utils/versionBundles.mjs"; + export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -109,15 +124,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(), @@ -132,6 +150,8 @@ export async function activate(context: ExtensionContext): Promise { new UninstallPicoSDKCommand(), new CleanCMakeCommand(ui), new UpdateOpenOCDCommand(), + new SbomTargetPathDebugCommand(), + new SbomTargetPathReleaseCommand(), ]; // register all command handlers @@ -169,6 +189,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, @@ -177,14 +208,14 @@ 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) { // finish activation - Logger.warn( - LoggerSource.extension, - "No workspace folder found." - ); + Logger.warn(LoggerSource.extension, "No workspace folder found."); await commands.executeCommand( "setContext", ContextKeys.isPicoProject, @@ -194,79 +225,187 @@ 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 ( - !readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes("pico_sdk_init()") - ) { - Logger.warn( - LoggerSource.extension, - "No pico_sdk_init() in CMakeLists.txt found." - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); + // check for pico_sdk_init() in CMakeLists.txt + if ( + !readFileSync(cmakeListsFilePath) + .toString("utf-8") + .includes("pico_sdk_init()") + ) { + Logger.warn( + LoggerSource.extension, + "No pico_sdk_init() in CMakeLists.txt found." + ); + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + false + ); - return; + 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 + ); + } + + 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 + await commands.executeCommand("setContext", ContextKeys.isPicoProject, true); + + if (isRustProject) { + const vs = new VersionBundlesLoader(context.extensionUri); + const latestSDK = await vs.getLatestSDK(); + if (!latestSDK) { + Logger.error( + LoggerSource.extension, + "Failed to get latest Pico SDK version for Rust project." + ); + void window.showErrorMessage( + "Failed to get latest Pico SDK version for Rust project." + ); + + return; + } + + const sdk = await window.withProgress( + { + location: ProgressLocation.Notification, + title: + "Downloading and installing latest Pico SDK (" + + latestSDK + + "). This may take a while...", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallSDK( + latestSDK, + SDK_REPOSITORY_URL + ); + + progress.report({ + increment: 100, + }); + + if (!result) { + installSuccess = false; + + Logger.error( + LoggerSource.extension, + "Failed to install latest SDK", + `version: ${latestSDK}.`, + "Make sure all requirements are met." + ); + + void window.showErrorMessage( + "Failed to install latest SDK version for rust project." + ); + + return false; + } else { + Logger.info( + LoggerSource.extension, + "Found/installed latest SDK", + `version: ${latestSDK}` + ); + + return true; + } + } ); - const wantToImport = await window.showInformationMessage( - "Do you want to import this project as Raspberry Pi Pico project?", - "Yes", - "No" + if (!sdk) { + return; + } + + const cargo = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Rust. This may take a while...", + cancellable: false, + }, + async () => downloadAndInstallRust() ); - if (wantToImport === "Yes") { - void commands.executeCommand( - `${extensionName}.${ImportProjectCommand.id}`, - workspaceFolder.uri - ); + 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; } - await commands.executeCommand("setContext", ContextKeys.isPicoProject, true); - // get sdk selected in the project const selectedToolchainAndSDKVersions = await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); @@ -495,7 +634,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({ @@ -796,7 +935,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 b6c45501..9258dec4 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,8 @@ import { HTTP_STATUS_UNAUTHORIZED, githubApiUnauthorized, HTTP_STATUS_FORBIDDEN, + HTTP_STATUS_OK, + SDK_REPOSITORY_URL, } from "./githubREST.mjs"; import { unxzFile, unzipFile } from "./downloadHelpers.mjs"; import type { Writable } from "stream"; @@ -42,10 +47,13 @@ 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"; +import findPython, { showPythonNotFoundError } from "./pythonHelper.mjs"; /// Translate nodejs platform names to ninja platform names const NINJA_PLATFORMS: { [key: string]: string } = { @@ -192,6 +200,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. * @@ -217,7 +249,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 ( @@ -236,7 +269,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; @@ -287,7 +320,8 @@ export async function downloadAndInstallArchive( const unpackResult = await unpackArchive( archiveFilePath, targetDirectory, - archiveExtension + archiveExtension, + xzSingleDirOption ); if (unpackResult && extraCallback) { @@ -412,11 +446,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; @@ -537,6 +576,8 @@ export async function downloadAndInstallSDK( (await cloneRepository(repositoryUrl, version, targetDirectory, gitPath)) ) { settings.reload(); + + // TODO: should already be done by rust project panel // check python requirements const python3Exe: string = python3Path || @@ -552,7 +593,10 @@ export async function downloadAndInstallSDK( "Python3 is not installed and could not be downloaded." ); - void window.showErrorMessage("Python3 is not installed and in PATH."); + void window.showErrorMessage( + "Python 3 was not found. " + + "Please install Python 3 to complete Pico SDK setup." + ); return false; } @@ -577,8 +621,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, @@ -1031,10 +1075,10 @@ export async function downloadAndInstallOpenOCD( ? "-aarch64" : "-x86_64" : process.platform === "darwin" - ? process.arch === "arm64" - ? "-arm64" - : "-x86_64" - : "" + ? process.arch === "arm64" + ? "-arm64" + : "-x86_64" + : "" }-${TOOLS_PLATFORMS[process.platform]}.${assetExt}`; const extraCallback = (): void => { @@ -1267,3 +1311,205 @@ 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; + } + + // install python (if necessary) + const python3Path = await findPython(); + if (!python3Path) { + Logger.error(LoggerSource.downloader, "Failed to find Python3 executable."); + showPythonNotFoundError(); + + return false; + } + + let result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing SDK", + cancellable: false, + }, + async progress2 => { + const result = await downloadAndInstallSDK( + latest[0], + SDK_REPOSITORY_URL, + // python3Path is only possible undefined if downloaded and + // there is already checked and returned if this happened + python3Path.replace(HOME_VAR, homedir().replaceAll("\\", "/")) + ); + + if (!result) { + Logger.error( + LoggerSource.downloader, + "Failed to download and install SDK." + ); + progress2.report({ + message: "Failed - Make sure all requirements are met.", + increment: 100, + }); + + void window.showErrorMessage( + "Failed to download and install SDK. " + + "Make sure all requirements are met." + ); + + return false; + } + + return true; + } + ); + + if (!result) { + return false; + } + + const supportedToolchains = await getSupportedToolchains(); + + result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Downloading ARM Toolchain for debugging...`, + }, + async progress => { + const toolchain = supportedToolchains.find( + t => t.version === latest[1].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[1].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[1].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..19ab639d 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"; } } @@ -221,9 +231,10 @@ async function getReleases(repository: GithubRepository): Promise { headers["if-none-match"] = lastEtag; } + const url = `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases`; // eslint-disable-next-line @typescript-eslint/naming-convention const response = await makeAsyncGetRequest>( - `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases`, + url, headers ); @@ -251,7 +262,9 @@ async function getReleases(repository: GithubRepository): Promise { // there is no way a rerun will succeed in the near future throw new Error("GitHub API Code 403 Forbidden. Rate limit exceeded."); } else if (response.status !== 200) { - throw new Error("Error http status code: " + response.status); + throw new Error( + "Error http status code: " + response.status + " for " + url + ); } if (response.data !== null) { @@ -307,6 +320,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..1e4f4719 --- /dev/null +++ b/src/utils/projectGeneration/projectRust.mts @@ -0,0 +1,1736 @@ +/* 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, { + SbomTargetPathDebugCommand, + SbomTargetPathReleaseCommand, +} 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: "Build + Generate SBOM (debug)", + consoleLogLevel: "Debug", + wireProtocol: "Swd", + }, + ], + }; + + const settings = { + "rust-analyzer.cargo.target": "thumbv8m.main-none-eabihf", + "rust-analyzer.check.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: "Build + Generate SBOM (release)", + type: "shell", + command: "bash", + args: [ + "-lc", + `cargo sbom > \${command:${extensionName}.${SbomTargetPathReleaseCommand.id}}`, + ], + windows: { + command: "powershell", + args: [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `cargo sbom | Set-Content -Encoding utf8 \${command:${extensionName}.${SbomTargetPathReleaseCommand.id}}`, + ], + }, + dependsOn: "Compile Project", + presentation: { + reveal: "silent", + panel: "shared", + }, + problemMatcher: [], + }, + { + 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: "Build + Generate SBOM (debug)", + type: "shell", + command: "bash", + args: [ + "-lc", + `cargo sbom --output-format spdx-json > \${command:${extensionName}.${SbomTargetPathDebugCommand.id}}`, + ], + windows: { + command: "powershell", + args: [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `cargo sbom | Set-Content -Encoding utf8 \${command:${extensionName}.${SbomTargetPathDebugCommand.id}}`, + ], + }, + dependsOn: "Compile Project (debug)", + presentation: { + reveal: "silent", + panel: "shared", + }, + problemMatcher: [], + }, + { + label: "Run Project", + type: "shell", + dependsOn: ["Build + Generate SBOM (release)"], + 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 = `//! SPDX-License-Identifier: MIT OR Apache-2.0 +//! +//! Copyright (c) 2021–2024 The rp-rs Developers +//! Copyright (c) 2021 rp-rs organization +//! Copyright (c) 2025 Raspberry Pi Ltd. +//! +//! # GPIO 'Blinky' Example +//! +//! This application demonstrates how to control a GPIO pin on the rp2040 and rp235x. +//! +//! It may need to be adapted to your particular board layout and/or pin assignment. + +#![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. +#[unsafe(link_section = ".boot2")] +#[used] +#[cfg(rp2040)] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +/// Tell the Boot ROM about our application +#[unsafe(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 rp2040 and 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\` +#[unsafe(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; + } +} + +async function generateLicenses(projectRoot: string): Promise { + const mitLicense = `MIT License + +Copyright (c) 2021–2024 The rp-rs Developers +Copyright (c) 2021 rp-rs organization +Copyright (c) 2025 Raspberry Pi Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + `; + + const apache2License = ` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2021–2024 The rp-rs Developers + Copyright (c) 2021 rp-rs organization + Copyright (c) 2025 Raspberry Pi Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.`; + + try { + await writeFile(join(projectRoot, "LICENSE-MIT"), mitLicense); + await writeFile(join(projectRoot, "LICENSE-APACHE"), apache2License); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write licenses", + 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: "2024", + 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: "1", + "defmt-rtt": "1", + }, + target: { + "'cfg( target_arch = \"arm\" )'": { + dependencies: { + "panic-probe": new TomlInlineObject({ + version: "1", + 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 = `/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 The rp-rs Developers +* Copyright (c) 2021 rp-rs organization +* Copyright (c) 2025 Raspberry Pi Ltd. +*/ + +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 = `/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 The rp-rs Developers +* Copyright (c) 2021 rp-rs organization +* Copyright (c) 2025 Raspberry Pi Ltd. +*/ + +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 = `/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 The rp-rs Developers +* Copyright (c) 2021 rp-rs organization +* Copyright (c) 2025 Raspberry Pi Ltd. +*/ + +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 = `//! SPDX-License-Identifier: MIT OR Apache-2.0 +//! +//! Copyright (c) 2021–2024 The rp-rs Developers +//! Copyright (c) 2021 rp-rs organization +//! Copyright (c) 2025 Raspberry Pi Ltd. +//! +//! Set up linker scripts + +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 = `# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2021–2024 The rp-rs Developers +# Copyright (c) 2021 rp-rs organization +# Copyright (c) 2025 Raspberry Pi Ltd. +# +# 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) +linker = "flip-link" +rustflags = [ + "-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 generateLicenses(projectFolder); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate licenses"); + + 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..c575b246 --- /dev/null +++ b/src/utils/rustUtil.mts @@ -0,0 +1,617 @@ +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; + } 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; + } + Logger.error( + LoggerSource.rustUtil, + `Failed to install cargo package '${packageName}': ${unknownErrorToString( + error + )}` + ); + + return unknownErrorToString(error); + } +} + +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 and restart VS Code.", + "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 { + const 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"; + let cargoInstResult = await cargoInstall(flipLink, false); + if (cargoInstResult !== undefined) { + if (cargoInstResult.includes("error: linker `link.exe` not found")) { + void window + .showErrorMessage( + `Failed to install cargo package '${flipLink}'` + + " because the MSVC linker is not found" + + " or Windows SDK components are missing.", + "More Info" + ) + .then(selection => { + if (selection === "More Info") { + env.openExternal( + Uri.parse( + // eslint-disable-next-line max-len + "https://rust-lang.github.io/rustup/installation/windows-msvc.html#manual-install", + true + ) + ); + } + }); + } else { + void window.showErrorMessage( + `Failed to install cargo package '${flipLink}'.` + + "Please check the logs." + ); + } + + return false; + } + + // or install probe-rs-tools + const probeRsTools = "defmt-print"; + cargoInstResult = await cargoInstall(probeRsTools, true); + if (cargoInstResult !== undefined) { + void window.showErrorMessage( + `Failed to install cargo package '${probeRsTools}'.` + + "Please check the logs." + ); + + return false; + } + + // install elf2uf2-rs + const elf2uf2Rs = "elf2uf2-rs"; + cargoInstResult = await cargoInstall(elf2uf2Rs, true); + if (cargoInstResult !== undefined) { + void window.showErrorMessage( + `Failed to install cargo package '${elf2uf2Rs}'.` + + "Please check the logs." + ); + + return false; + } + + // install cargo-sbom + const cargoSbom = "cargo-sbom"; + cargoInstResult = await cargoInstall(cargoSbom, true); + if (cargoInstResult !== undefined) { + void window.showErrorMessage( + `Failed to install cargo package '${cargoSbom}'.` + + "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 4d81f235..eddd5f2e 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.18.0"; +export const OPENOCD_VERSION = "0.12.0+dev"; diff --git a/src/utils/versionBundles.mts b/src/utils/versionBundles.mts index e5cade02..b7ff3653 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/" + @@ -127,4 +128,24 @@ export default class VersionBundlesLoader { return chosenBundle; } + + public async getLatest(): Promise<[string, VersionBundle] | undefined> { + if (this.bundles === undefined) { + await this.loadBundles(); + } + + return Object.entries(this.bundles ?? {}).sort( + (a, b) => compare(a[0], b[0]) * -1 + )[0]; + } + + 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]; + } } 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 b4005898..dc14cff7 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; @@ -1056,7 +1055,7 @@ export class NewProjectPanel { }, async progress2 => { const result = await downloadAndInstallOpenOCD( - openOCDVersion, + OPENOCD_VERSION, (prog: GotProgress) => { const per = prog.percent * 100; progress2.report({ @@ -1307,7 +1306,7 @@ export class NewProjectPanel { sdkVersion: selectedSDK, sdkPath: buildSDKPath(selectedSDK), picotoolVersion: selectedPicotool, - openOCDVersion: openOCDVersion, + openOCDVersion: OPENOCD_VERSION, }, ninjaExecutable, cmakeExecutable, @@ -1332,7 +1331,7 @@ export class NewProjectPanel { sdkVersion: selectedSDK, sdkPath: buildSDKPath(selectedSDK), picotoolVersion: selectedPicotool, - openOCDVersion: openOCDVersion, + openOCDVersion: OPENOCD_VERSION, }, ninjaExecutable, cmakeExecutable, @@ -1353,7 +1352,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..3e585d7b --- /dev/null +++ b/src/webview/newRustProjectPanel.mts @@ -0,0 +1,558 @@ +/* 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 { generateRustProject } from "../utils/projectGeneration/projectRust.mjs"; +import { installLatestRustRequirements } from "../utils/download.mjs"; + +interface SubmitMessageValue { + projectName: string; +} + +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); + + 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: Ensure that you have the latest version of the Rustup toolchain manager installed. +

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+ + +
+
+ + + + `; + } +} 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..82f6b489 --- /dev/null +++ b/web/rust/main.js @@ -0,0 +1,141 @@ +"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 nameRaw = projectNameElement.value || ""; + const name = nameRaw.trim(); + + // Windows reserved basenames (case-insensitive) + const reservedNames = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i; + + // Valid Cargo crate/package name: + // - start with a letter + // - rest: letters, digits, underscore, hyphen + // - all lowercase (Cargo convention) + const crateNameRegex = /^[a-z][a-z0-9_-]*$/; + + // Disallow path separators and whitespace outright as a fast path + const hasBadChars = /[\/\\:*?"<>|\s]/.test(name); + + if ( + name.length === 0 || + hasBadChars || + reservedNames.test(name) || + !crateNameRegex.test(name) + ) { + projectNameError.hidden = false; + projectNameError.innerHTML = + `Error \ +Project name must start with a letter and contain only lowercase letters, digits, '-' or '_' (no spaces), \ +and must not be a reserved Windows name.`; + 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; + } + + //post all data values to the extension + vscode.postMessage({ + command: CMD_SUBMIT, + value: { + projectName: projectName + } + }); + } + + 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); +}());