From f076f531a7783df957c46af38172aa317c2f9fdf Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:14:39 +0100 Subject: [PATCH 01/27] Added experimental rust support Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- .vscodeignore | 1 + README.md | 270 +++++----- package.json | 14 +- scripts/portable-msvc.py | 372 ++++++++++++++ src/commands/compileProject.mts | 9 + src/commands/newProject.mts | 14 +- src/contextKeys.mts | 1 + src/extension.mts | 121 +++-- src/logger.mts | 1 + src/state.mts | 14 + src/ui.mts | 17 +- src/utils/download.mts | 40 +- src/utils/downloadHelpers.mts | 81 +-- src/utils/githubREST.mts | 9 + src/utils/rustUtil.mts | 586 ++++++++++++++++++++++ src/webview/activityBar.mts | 13 + src/webview/newRustProjectPanel.mts | 742 ++++++++++++++++++++++++++++ web/rust/main.js | 130 +++++ 18 files changed, 2212 insertions(+), 223 deletions(-) create mode 100644 scripts/portable-msvc.py create mode 100644 src/state.mts create mode 100644 src/utils/rustUtil.mts create mode 100644 src/webview/newRustProjectPanel.mts create mode 100644 web/rust/main.js diff --git a/.vscodeignore b/.vscodeignore index 9fbca977..790036bb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -32,6 +32,7 @@ node_modules/** !scripts/btstack_config.h !scripts/pico_configs.tsv !scripts/pico_project.py +!scripts/portable-msvc.py !scripts/pico-vscode.cmake !scripts/Pico.code-profile !scripts/raspberrypi-swd.cfg diff --git a/README.md b/README.md index 71b71db6..2dbe54ff 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,138 @@ -# 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 + +### Linux + +- **GCC** for the host architecture + +## 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.json b/package.json index 6d309897..65fa2ef0 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", @@ -157,7 +158,7 @@ "command": "raspberry-pi-pico.runProject", "title": "Run Pico Project (USB)", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.clearGithubApiCache", @@ -168,7 +169,7 @@ "command": "raspberry-pi-pico.conditionalDebugging", "title": "Conditional Debugging", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !inQuickOpen" + "enablement": "raspberry-pi-pico.isPicoProject && !inQuickOpen && !raspberry-pi-pico.isRustProject" }, { "command": "raspberry-pi-pico.debugLayout", @@ -185,7 +186,7 @@ "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", @@ -217,7 +218,7 @@ "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", @@ -331,6 +332,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/scripts/portable-msvc.py b/scripts/portable-msvc.py new file mode 100644 index 00000000..c58bbb6a --- /dev/null +++ b/scripts/portable-msvc.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 + +# Copyright https://gist.github.com/mmozeiko/7f3162ec2988e81e56d5c4e22cde9977 2024 + +import io +import os +import sys +import stat +import json +import shutil +import hashlib +import zipfile +import tempfile +import argparse +import subprocess +import urllib.error +import urllib.request +from pathlib import Path + +OUTPUT = Path("msvc") # output folder +DOWNLOADS = Path("downloads") # temporary download files + +# NOTE: not all host & target architecture combinations are supported + +DEFAULT_HOST = "x64" +ALL_HOSTS = "x64 x86 arm64".split() + +DEFAULT_TARGET = "x64" +ALL_TARGETS = "x64 x86 arm arm64".split() + +MANIFEST_URL = "https://aka.ms/vs/17/release/channel" +MANIFEST_PREVIEW_URL = "https://aka.ms/vs/17/pre/channel" + +ssl_context = None + +def download(url): + with urllib.request.urlopen(url, context=ssl_context) as res: + return res.read() + +total_download = 0 + +def download_progress(url, check, filename): + fpath = DOWNLOADS / filename + if fpath.exists(): + data = fpath.read_bytes() + if hashlib.sha256(data).hexdigest() == check.lower(): + print(f"\r{filename} ... OK") + return data + + global total_download + with fpath.open("wb") as f: + data = io.BytesIO() + with urllib.request.urlopen(url, context=ssl_context) as res: + total = int(res.headers["Content-Length"]) + size = 0 + while True: + block = res.read(1<<20) + if not block: + break + f.write(block) + data.write(block) + size += len(block) + perc = size * 100 // total + print(f"\r{filename} ... {perc}%", end="") + print() + data = data.getvalue() + digest = hashlib.sha256(data).hexdigest() + if check.lower() != digest: + exit(f"Hash mismatch for f{pkg}") + total_download += len(data) + return data + +# super crappy msi format parser just to find required .cab files +def get_msi_cabs(msi): + index = 0 + while True: + index = msi.find(b".cab", index+4) + if index < 0: + return + yield msi[index-32:index+4].decode("ascii") + +def first(items, cond = lambda x: True): + return next((item for item in items if cond(item)), None) + + +### parse command-line arguments + +ap = argparse.ArgumentParser() +ap.add_argument("--show-versions", action="store_true", help="Show available MSVC and Windows SDK versions") +ap.add_argument("--accept-license", action="store_true", help="Automatically accept license") +ap.add_argument("--msvc-version", help="Get specific MSVC version") +ap.add_argument("--sdk-version", help="Get specific Windows SDK version") +ap.add_argument("--preview", action="store_true", help="Use preview channel for Preview versions") +ap.add_argument("--target", default=DEFAULT_TARGET, help=f"Target architectures, comma separated ({','.join(ALL_TARGETS)})") +ap.add_argument("--host", default=DEFAULT_HOST, help=f"Host architecture", choices=ALL_HOSTS) +args = ap.parse_args() + +host = args.host +targets = args.target.split(',') +for target in targets: + if target not in ALL_TARGETS: + exit(f"Unknown {target} target architecture!") + + +### get main manifest + +URL = MANIFEST_PREVIEW_URL if args.preview else MANIFEST_URL + +try: + manifest = json.loads(download(URL)) +except urllib.error.URLError as err: + import ssl + if isinstance(err.args[0], ssl.SSLCertVerificationError): + # for more info about Python & issues with Windows certificates see https://stackoverflow.com/a/52074591 + print("ERROR: ssl certificate verification error") + try: + import certifi + except ModuleNotFoundError: + print("ERROR: please install 'certifi' package to use Mozilla certificates") + print("ERROR: or update your Windows certs, see instructions here: https://woshub.com/updating-trusted-root-certificates-in-windows-10/#h2_3") + exit() + print("NOTE: retrying with certifi certificates") + ssl_context = ssl.create_default_context(cafile=certifi.where()) + manifest = json.loads(download(URL)) + else: + raise + +### download VS manifest + +ITEM_NAME = "Microsoft.VisualStudio.Manifests.VisualStudioPreview" if args.preview else "Microsoft.VisualStudio.Manifests.VisualStudio" + +vs = first(manifest["channelItems"], lambda x: x["id"] == ITEM_NAME) +payload = vs["payloads"][0]["url"] + +vsmanifest = json.loads(download(payload)) + + +### find MSVC & WinSDK versions + +packages = {} +for p in vsmanifest["packages"]: + packages.setdefault(p["id"].lower(), []).append(p) + +msvc = {} +sdk = {} + +for pid,p in packages.items(): + if pid.startswith("Microsoft.VisualStudio.Component.VC.".lower()) and pid.endswith(".x86.x64".lower()): + pver = ".".join(pid.split(".")[4:6]) + if pver[0].isnumeric(): + msvc[pver] = pid + elif pid.startswith("Microsoft.VisualStudio.Component.Windows10SDK.".lower()) or \ + pid.startswith("Microsoft.VisualStudio.Component.Windows11SDK.".lower()): + pver = pid.split(".")[-1] + if pver.isnumeric(): + sdk[pver] = pid + +if args.show_versions: + print("MSVC versions:", " ".join(sorted(msvc.keys()))) + print("Windows SDK versions:", " ".join(sorted(sdk.keys()))) + exit(0) + +msvc_ver = args.msvc_version or max(sorted(msvc.keys())) +sdk_ver = args.sdk_version or max(sorted(sdk.keys())) + +if msvc_ver in msvc: + msvc_pid = msvc[msvc_ver] + msvc_ver = ".".join(msvc_pid.split(".")[4:-2]) +else: + exit(f"Unknown MSVC version: f{args.msvc_version}") + +if sdk_ver in sdk: + sdk_pid = sdk[sdk_ver] +else: + exit(f"Unknown Windows SDK version: f{args.sdk_version}") + +print(f"Downloading MSVC v{msvc_ver} and Windows SDK v{sdk_ver}") + + +### agree to license + +tools = first(manifest["channelItems"], lambda x: x["id"] == "Microsoft.VisualStudio.Product.BuildTools") +resource = first(tools["localizedResources"], lambda x: x["language"] == "en-us") +license = resource["license"] + +if not args.accept_license: + accept = input(f"Do you accept Visual Studio license at {license} [Y/N] ? ") + if not accept or accept[0].lower() != "y": + exit(0) + +OUTPUT.mkdir(exist_ok=True) +DOWNLOADS.mkdir(exist_ok=True) + + +### download MSVC + +msvc_packages = [ + f"microsoft.visualcpp.dia.sdk", + f"microsoft.vc.{msvc_ver}.crt.headers.base", + f"microsoft.vc.{msvc_ver}.crt.source.base", + f"microsoft.vc.{msvc_ver}.asan.headers.base", + f"microsoft.vc.{msvc_ver}.pgo.headers.base", +] + +for target in targets: + msvc_packages += [ + f"microsoft.vc.{msvc_ver}.tools.host{host}.target{target}.base", + f"microsoft.vc.{msvc_ver}.tools.host{host}.target{target}.res.base", + f"microsoft.vc.{msvc_ver}.crt.{target}.desktop.base", + f"microsoft.vc.{msvc_ver}.crt.{target}.store.base", + f"microsoft.vc.{msvc_ver}.premium.tools.host{host}.target{target}.base", + f"microsoft.vc.{msvc_ver}.pgo.{target}.base", + ] + if target in ["x86", "x64"]: + msvc_packages += [f"microsoft.vc.{msvc_ver}.asan.{target}.base"] + + redist_suffix = ".onecore.desktop" if target == "arm" else "" + redist_pkg = f"microsoft.vc.{msvc_ver}.crt.redist.{target}{redist_suffix}.base" + if redist_pkg not in packages: + redist_name = f"microsoft.visualcpp.crt.redist.{target}{redist_suffix}" + redist = first(packages[redist_name]) + redist_pkg = first(redist["dependencies"], lambda dep: dep.endswith(".base")).lower() + msvc_packages += [redist_pkg] + +for pkg in sorted(msvc_packages): + if pkg not in packages: + print(f"\r{pkg} ... !!! MISSING !!!") + continue + p = first(packages[pkg], lambda p: p.get("language") in (None, "en-US")) + for payload in p["payloads"]: + filename = payload["fileName"] + download_progress(payload["url"], payload["sha256"], filename) + with zipfile.ZipFile(DOWNLOADS / filename) as z: + for name in z.namelist(): + if name.startswith("Contents/"): + out = OUTPUT / Path(name).relative_to("Contents") + out.parent.mkdir(parents=True, exist_ok=True) + out.write_bytes(z.read(name)) + + +### download Windows SDK + +sdk_packages = [ + f"Windows SDK for Windows Store Apps Tools-x86_en-us.msi", + f"Windows SDK for Windows Store Apps Headers-x86_en-us.msi", + f"Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi", + f"Windows SDK for Windows Store Apps Libs-x86_en-us.msi", + f"Windows SDK OnecoreUap Headers x86-x86_en-us.msi", + f"Windows SDK Desktop Headers x86-x86_en-us.msi", + f"Universal CRT Headers Libraries and Sources-x86_en-us.msi", +] + +for target in targets: + sdk_packages += [f"Windows SDK Desktop Libs {target}-x86_en-us.msi"] + +with tempfile.TemporaryDirectory(dir=DOWNLOADS) as d: + dst = Path(d) + + sdk_pkg = packages[sdk_pid][0] + sdk_pkg = packages[first(sdk_pkg["dependencies"]).lower()][0] + + msi = [] + cabs = [] + + # download msi files + for pkg in sorted(sdk_packages): + payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}") + if payload is None: + continue + msi.append(DOWNLOADS / pkg) + data = download_progress(payload["url"], payload["sha256"], pkg) + cabs += list(get_msi_cabs(data)) + + # download .cab files + for pkg in cabs: + payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}") + download_progress(payload["url"], payload["sha256"], pkg) + + print("Unpacking msi files...") + + # run msi installers + for m in msi: + subprocess.check_call(["msiexec.exe", "/a", m, "/quiet", "/qn", f"TARGETDIR={OUTPUT.resolve()}"]) + (OUTPUT / m.name).unlink() + + +### versions + +msvcv = first((OUTPUT / "VC/Tools/MSVC").glob("*")).name +sdkv = first((OUTPUT / "Windows Kits/10/bin").glob("*")).name + + +# place debug CRT runtime files into MSVC bin folder (not what real Visual Studio installer does... but is reasonable) +# NOTE: these are Target architecture, not Host architecture binaries + +redist = OUTPUT / "VC/Redist" + +if redist.exists(): + redistv = first((redist / "MSVC").glob("*")).name + src = redist / "MSVC" / redistv / "debug_nonredist" + for target in targets: + for f in (src / target).glob("**/*.dll"): + dst = OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}" / target + f.replace(dst / f.name) + + shutil.rmtree(redist) + + +# copy msdia140.dll file into MSVC bin folder +# NOTE: this is meant only for development - always Host architecture, even when placed into all Target architecture folders + +msdia140dll = { + "x86": "msdia140.dll", + "x64": "amd64/msdia140.dll", + "arm": "arm/msdia140.dll", + "arm64": "arm64/msdia140.dll", +} + +dst = OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}" +src = OUTPUT / "DIA%20SDK/bin" / msdia140dll[host] +for target in targets: + shutil.copyfile(src, dst / target / src.name) + +shutil.rmtree(OUTPUT / "DIA%20SDK") + + +### cleanup + +shutil.rmtree(OUTPUT / "Common7", ignore_errors=True) +shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / "Auxiliary") +for target in targets: + for f in [f"store", "uwp", "enclave", "onecore"]: + shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / "lib" / target / f, ignore_errors=True) + shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}" / target / "onecore", ignore_errors=True) +for f in ["Catalogs", "DesignTime", f"bin/{sdkv}/chpe", f"Lib/{sdkv}/ucrt_enclave"]: + shutil.rmtree(OUTPUT / "Windows Kits/10" / f, ignore_errors=True) +for arch in ["x86", "x64", "arm", "arm64"]: + if arch not in targets: + shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "ucrt" / arch, ignore_errors=True) + shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "um" / arch, ignore_errors=True) + if arch != host: + shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{arch}", ignore_errors=True) + shutil.rmtree(OUTPUT / "Windows Kits/10/bin" / sdkv / arch, ignore_errors=True) + +# executable that is collecting & sending telemetry every time cl/link runs +for target in targets: + (OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}/{target}/vctip.exe").unlink(missing_ok=True) + + +### setup.bat + +for target in targets: + + SETUP = fr"""@echo off + +set VSCMD_ARG_HOST_ARCH={host} +set VSCMD_ARG_TGT_ARCH={target} + +set VCToolsVersion={msvcv} +set WindowsSDKVersion={sdkv}\ + +set VCToolsInstallDir=%~dp0VC\Tools\MSVC\{msvcv}\ +set WindowsSdkBinPath=%~dp0Windows Kits\10\bin\ + +set PATH=%~dp0VC\Tools\MSVC\{msvcv}\bin\Host{host}\{target};%~dp0Windows Kits\10\bin\{sdkv}\{host};%~dp0Windows Kits\10\bin\{sdkv}\{host}\ucrt;%PATH% +set INCLUDE=%~dp0VC\Tools\MSVC\{msvcv}\include;%~dp0Windows Kits\10\Include\{sdkv}\ucrt;%~dp0Windows Kits\10\Include\{sdkv}\shared;%~dp0Windows Kits\10\Include\{sdkv}\um;%~dp0Windows Kits\10\Include\{sdkv}\winrt;%~dp0Windows Kits\10\Include\{sdkv}\cppwinrt +set LIB=%~dp0VC\Tools\MSVC\{msvcv}\lib\{target};%~dp0Windows Kits\10\Lib\{sdkv}\ucrt\{target};%~dp0Windows Kits\10\Lib\{sdkv}\um\{target} +""" + (OUTPUT / f"setup_{target}.bat").write_text(SETUP) + +print(f"Total downloaded: {total_download>>20} MB") +print("Done!") diff --git a/src/commands/compileProject.mts b/src/commands/compileProject.mts index 988e8d34..93138316 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -3,7 +3,10 @@ import { EventEmitter } from "events"; import { CommandWithResult } from "./command.mjs"; import Logger from "../logger.mjs"; import Settings, { SettingsKey } from "../settings.mjs"; +import { ContextKeys } from "../contextKeys.mjs"; +import State from "../state.mjs"; import { cmakeToolsForcePicoKit } from "../utils/cmakeToolsUtil.mjs"; + export default class CompileProjectCommand extends CommandWithResult { private _logger: Logger = new Logger("CompileProjectCommand"); @@ -18,9 +21,15 @@ export default class CompileProjectCommand extends CommandWithResult { const task = (await tasks.fetchTasks()).find( task => task.name === "Compile Project" ); + /*const isRustProject = await commands.executeCommand( + "getContext", + ContextKeys.isRustProject + );*/ + const isRustProject = State.getInstance().isRustProject; const settings = Settings.getInstance(); if ( + !isRustProject && settings !== undefined && settings.getBoolean(SettingsKey.useCmakeTools) ) { diff --git a/src/commands/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/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..ef3dfab0 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -83,7 +83,10 @@ 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 } from "./utils/rustUtil.mjs"; +import State from "./state.mjs"; import { cmakeToolsForcePicoKit } from "./utils/cmakeToolsUtil.mjs"; + export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -177,6 +180,9 @@ export async function activate(context: ExtensionContext): Promise { ); const workspaceFolder = workspace.workspaceFolders?.[0]; + const isRustProject = workspaceFolder + ? existsSync(join(workspaceFolder.uri.fsPath, ".pico-rs")) + : false; // check if there is a workspace folder if (workspaceFolder === undefined) { @@ -194,20 +200,31 @@ export async function activate(context: ExtensionContext): Promise { return; } - const cmakeListsFilePath = join(workspaceFolder.uri.fsPath, "CMakeLists.txt"); - if (!existsSync(cmakeListsFilePath)) { - Logger.warn( - LoggerSource.extension, - "No CMakeLists.txt in workspace folder has been found." - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false + /*void commands.executeCommand( + "setContext", + ContextKeys.isRustProject, + isRustProject + );*/ + State.getInstance().isRustProject = isRustProject; + + if (!isRustProject) { + const cmakeListsFilePath = join( + workspaceFolder.uri.fsPath, + "CMakeLists.txt" ); + if (!existsSync(cmakeListsFilePath)) { + Logger.warn( + LoggerSource.extension, + "No CMakeLists.txt in workspace folder has been found." + ); + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + false + ); - return; - } + return; + } // check for pico_sdk_init() in CMakeLists.txt if ( @@ -228,45 +245,59 @@ export async function activate(context: ExtensionContext): Promise { return; } - // check if it has .vscode folder and cmake donotedit header in CMakelists.txt - if ( - !existsSync(join(workspaceFolder.uri.fsPath, ".vscode")) || - !( - readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX) || - readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX_OLD) - ) - ) { - Logger.warn( - LoggerSource.extension, - "No .vscode folder and/or cmake", - '"DO NOT EDIT"-header in CMakelists.txt found.' - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); - const wantToImport = await window.showInformationMessage( - "Do you want to import this project as Raspberry Pi Pico project?", - "Yes", - "No" - ); - if (wantToImport === "Yes") { - void commands.executeCommand( - `${extensionName}.${ImportProjectCommand.id}`, - workspaceFolder.uri + // check if it has .vscode folder and cmake donotedit header in CMakelists.txt + if ( + !existsSync(join(workspaceFolder.uri.fsPath, ".vscode")) || + !( + readFileSync(cmakeListsFilePath) + .toString("utf-8") + .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX) || + readFileSync(cmakeListsFilePath) + .toString("utf-8") + .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX_OLD) + ) + ) { + Logger.warn( + LoggerSource.extension, + "No .vscode folder and/or cmake", + '"DO NOT EDIT"-header in CMakelists.txt found.' ); - } + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + false + ); + const wantToImport = await window.showInformationMessage( + "Do you want to import this project as Raspberry Pi Pico project?", + "Yes", + "No" + ); + if (wantToImport === "Yes") { + void commands.executeCommand( + `${extensionName}.${ImportProjectCommand.id}`, + workspaceFolder.uri + ); + } - return; + return; + } } await commands.executeCommand("setContext", ContextKeys.isPicoProject, true); + if (isRustProject) { + const cargo = await downloadAndInstallRust(); + if (!cargo) { + void window.showErrorMessage("Failed to install Rust."); + + return; + } + + ui.showStatusBarItems(isRustProject); + + return; + } + // get sdk selected in the project const selectedToolchainAndSDKVersions = await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); diff --git a/src/logger.mts b/src/logger.mts index a836b7c0..5c10d8ef 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -42,6 +42,7 @@ export enum LoggerSource { pythonHelper = "pythonHelper", gitUtil = "gitUtil", vscodeConfigUtil = "vscodeConfigUtil", + rustUtil = "rustUtil", } /** 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..92db3762 100644 --- a/src/ui.mts +++ b/src/ui.mts @@ -10,29 +10,38 @@ enum StatusBarItemKey { } const STATUS_BAR_ITEMS: { - [key: string]: { text: string; command: string; tooltip: string }; + [key: string]: { + text: 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: ", command: "raspberry-pi-pico.switchBoard", tooltip: "Select Board", + rustSupport: false, }, }; @@ -57,8 +66,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 { diff --git a/src/utils/download.mts b/src/utils/download.mts index b6c45501..7fbda2f9 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -34,6 +34,7 @@ import { HTTP_STATUS_UNAUTHORIZED, githubApiUnauthorized, HTTP_STATUS_FORBIDDEN, + HTTP_STATUS_OK, } from "./githubREST.mjs"; import { unxzFile, unzipFile } from "./downloadHelpers.mjs"; import type { Writable } from "stream"; @@ -192,6 +193,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 +242,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 ( @@ -287,7 +313,8 @@ export async function downloadAndInstallArchive( const unpackResult = await unpackArchive( archiveFilePath, targetDirectory, - archiveExtension + archiveExtension, + xzSingleDirOption ); if (unpackResult && extraCallback) { @@ -412,11 +439,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; 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..839baa1a 100644 --- a/src/utils/githubREST.mts +++ b/src/utils/githubREST.mts @@ -25,6 +25,7 @@ export enum GithubRepository { ninja = 2, tools = 3, picotool = 4, + rust = 5, } /** @@ -68,6 +69,8 @@ export function ownerOfRepository(repository: GithubRepository): string { return "Kitware"; case GithubRepository.ninja: return "ninja-build"; + case GithubRepository.rust: + return "rust-lang"; } } @@ -90,6 +93,8 @@ export function repoNameOfRepository(repository: GithubRepository): string { return "pico-sdk-tools"; case GithubRepository.picotool: return "picotool"; + case GithubRepository.rust: + return "rust"; } } @@ -307,6 +312,10 @@ export async function getCmakeReleases(): Promise { return getReleases(GithubRepository.cmake); } +export async function getRustReleases(): Promise { + return getReleases(GithubRepository.rust); +} + /** * Get the release data for a specific tag from * the GitHub RESY API. diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts new file mode 100644 index 00000000..49017f6b --- /dev/null +++ b/src/utils/rustUtil.mts @@ -0,0 +1,586 @@ +import { homedir } from "os"; +import { + downloadAndInstallArchive, + downloadAndReadFile, + getScriptsRoot, +} from "./download.mjs"; +import { getRustReleases } from "./githubREST.mjs"; +import { join as joinPosix } from "path/posix"; +import { + existsSync, + mkdirSync, + readdirSync, + renameSync, + rmSync, + symlinkSync, +} from "fs"; +import Logger, { LoggerSource } from "../logger.mjs"; +import { unknownErrorToString } from "./errorHelper.mjs"; +import { ProgressLocation, window } from "vscode"; +import type { Progress as GotProgress } from "got"; +import { parse as parseToml } from "toml"; +import { promisify } from "util"; +import { exec } from "child_process"; +import { dirname, join } from "path"; +import { copyFile, mkdir, readdir, rm, stat } from "fs/promises"; +import findPython from "./pythonHelper.mjs"; + +const STABLE_INDEX_DOWNLOAD_URL = + "https://static.rust-lang.org/dist/channel-rust-stable.toml"; + +const execAsync = promisify(exec); + +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( + cargoExecutable: string, + packageName: string, + locked = false, + moreEnv: { [key: string]: string } +): Promise { + const prefix = process.platform === "win32" ? "&" : ""; + const command = `${prefix}"${cargoExecutable}" install ${ + locked ? "--locked " : "" + }${packageName}`; + + let customEnv = process.env; + customEnv.PATH += `${process.platform === "win32" ? ";" : ":"}${dirname( + cargoExecutable + )}`; + if ("PATH" in moreEnv) { + customEnv.PATH += `${process.platform === "win32" ? ";" : ":"}${ + moreEnv.PATH + }`; + delete moreEnv.PATH; + } + if (moreEnv) { + // TODO: what with duplicates? + customEnv = { + ...customEnv, + ...moreEnv, + }; + } + try { + const { stdout, stderr } = await execAsync(command, { + shell: process.platform === "win32" ? "powershell.exe" : undefined, + windowsHide: true, + env: { + ...customEnv, + }, + }); + + // TODO: use better indication + if (stderr && !stderr.includes("to your PATH")) { + Logger.error( + LoggerSource.rustUtil, + `Failed to install cargo command '${command}': ${stderr}` + ); + + return false; + } + + Logger.debug(LoggerSource.rustUtil, `Cargo install output: ${stdout}`); + + return true; + } catch (error) { + const msg = unknownErrorToString(error); + if (msg.includes("to your PATH")) { + Logger.debug( + LoggerSource.rustUtil, + `Cargo command '${command}' failed but ignoring:`, + msg + ); + + return true; + } else { + Logger.error( + LoggerSource.rustUtil, + `Failed to install cargo command '${command}': ${msg}` + ); + + return false; + } + } +} + +export function detectExistingRustInstallation(): boolean { + const dir = joinPosix(homedir(), ".pico-sdk", "rust"); + + try { + const contents = readdirSync(dir); + + // Check if the directory contains a subdirectory if yes delete it + if (contents.length > 0) { + for (const itemToDelete of contents) { + const itemPath = joinPosix(dir, itemToDelete); + try { + rmSync(itemPath, { recursive: true, force: true }); + } catch (error) { + Logger.debug( + LoggerSource.rustUtil, + "Error deleting existing Rust installation:", + unknownErrorToString(error) + ); + } + } + + return true; + } else { + return false; + } + } catch { + return false; + } +} + +/** + * Merges multiple directories at the top level into a single directory structure. + * + * @param parentDir - The path to the parent directory containing the subdirectories to merge. + */ +async function mergeDirectories(parentDir: string): Promise { + // Get a list of all directories in the parent directory + const directories: string[] = (await readdir(parentDir)).filter(async dir => { + const stats = await stat(join(parentDir, dir)); + + return stats.isDirectory(); + }); + + // Define the subdirectories we want to merge + const subDirs: string[] = ["bin", "etc", "lib", "libexec", "share"]; + + // Create the top-level directories if they do not exist + await Promise.all( + subDirs.map(async subDir => { + const destSubDir: string = join(parentDir, subDir); + try { + await mkdir(destSubDir, { recursive: true }); + } catch { + // Ignore error if directory already exists + } + }) + ); + + // Function to merge directories + const mergeSubDirectories = async ( + srcDir: string, + destDir: string + ): Promise => { + const items: string[] = await readdir(srcDir); + + await Promise.all( + items.map(async item => { + const srcItemPath: string = join(srcDir, item); + const destItemPath: string = join(destDir, item); + + const stats = await stat(srcItemPath); + if (stats.isDirectory()) { + // If the item is a directory, merge it recursively + await mkdir(destItemPath, { recursive: true }); + await mergeSubDirectories(srcItemPath, destItemPath); + } else { + // If it's a file, copy it + await copyFile(srcItemPath, destItemPath); + } + }) + ); + }; + + // Merge the contents of the subdirectories into the top-level structure + await Promise.all( + directories.map(async directory => { + const dirPath: string = join(parentDir, directory); + + await Promise.all( + subDirs.map(async subDir => { + const sourcePath: string = join(dirPath, subDir); + + try { + const stats = await stat(sourcePath); + if (stats.isDirectory()) { + await mergeSubDirectories(sourcePath, join(parentDir, subDir)); + } + } catch { + // Ignore error if directory does not exist + } + }) + ); + }) + ); + + // Remove the old directories after merging their contents + await Promise.all( + directories.map(async directory => { + await rm(join(parentDir, directory), { + recursive: true, + force: true, + }); + }) + ); +} + +// TODO: move task env setup for this into a command +async function installPortableMSVC(): Promise { + const python = await findPython(); + if (!python) { + Logger.error(LoggerSource.rustUtil, "Could not find python"); + + return false; + } + + const prefix = process.platform === "win32" ? "&" : ""; + // TODO: ask for license + const command = `${prefix}"${python}" "${joinPosix( + getScriptsRoot(), + "portable-msvc.py" + )}" --accept-license --msvc-version 14.41 --sdk-version 19041`; + + try { + const newBase = join(homedir(), ".pico-sdk", "msvc"); + mkdirSync(newBase, { recursive: true }); + + const { stdout, stderr } = await execAsync(command, { + shell: process.platform === "win32" ? "powershell.exe" : undefined, + windowsHide: true, + cwd: join(homedir(), ".pico-sdk", "msvc"), + }); + + if (stderr) { + Logger.error( + LoggerSource.rustUtil, + `Failed to install MSVC '${command}': ${stderr}` + ); + + return false; + } + + const newPath = join(newBase, "14.41"); + // move getScriptsRoot()/msvc to ~/.pico-sdk/msvc/14.41 and symlink to msvc/latest + renameSync(join(newBase, "msvc"), newPath); + + symlinkSync(newPath, join(newBase, "latest"), "junction"); + + return true; + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + "Failed to install MSVC:", + unknownErrorToString(error) + ); + + return false; + } +} + +/** + * Download and installs the latest version of Rust. + * + * @returns A promise that resolves to an object containing the + * paths to the installed `rustc` and `cargo` executables, + * or `undefined` if the installation failed. + */ +export async function downloadAndInstallRust(): Promise { + // TODO: use channel rust stable instead + const rustReleases = await getRustReleases(); + const latestRelease = rustReleases[0]; + + const downloadLink = computeDownloadLink(latestRelease); + const targetDirectory = joinPosix( + homedir(), + ".pico-sdk", + "rust", + latestRelease + ); + + if (existsSync(targetDirectory)) { + Logger.debug( + LoggerSource.rustUtil, + `Latest Rust ${latestRelease} already installed, skipping installation` + ); + + return joinPosix( + targetDirectory, + "bin", + "cargo" + (process.platform === "win32" ? ".exe" : "") + ); + } + + const existingInstallation = detectExistingRustInstallation(); + + // Download and install Rust + let progressState = 0; + const result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Rust", + cancellable: false, + }, + async progress => + downloadAndInstallArchive( + downloadLink, + targetDirectory, + `rust-${latestRelease}.tar.xz`, + "Rust", + undefined, + undefined, + undefined, + (prog: GotProgress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + } + ) + ); + if (!result) { + return undefined; + } + + const index = await downloadAndReadFile(STABLE_INDEX_DOWNLOAD_URL); + if (!index) { + // TODO: undo rust download + return undefined; + } + Logger.debug(LoggerSource.rustUtil, "Downloaded Rust index file"); + + try { + const data: IndexToml = parseToml(index) as IndexToml; + + const targetTriple = "thumbv6m-none-eabi"; + if ( + data.pkg && + data.pkg["rust-std"] && + data.pkg["rust-std"].target && + data.pkg["rust-std"].target[targetTriple] && + data.pkg["rust-std"].target[targetTriple].available + ) { + const stdDownloadLink = data.pkg["rust-std"].target[targetTriple].xz_url; + progressState = 0; + const targetLabel = `rust-std-${targetTriple}`; + const newTargetDirectory = joinPosix(targetDirectory, targetLabel); + const result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Pico Rust target stdlib", + cancellable: false, + }, + async progress => + downloadAndInstallArchive( + stdDownloadLink, + newTargetDirectory, + `rust-pico-${latestRelease}-std.tar.xz`, + "Rust Pico Standard Library", + undefined, + undefined, + undefined, + (prog: GotProgress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }, + `rust-std-${latestRelease}-${targetTriple}/${targetLabel}` + ) + ); + + if (!result) { + return undefined; + } + } else { + Logger.error( + LoggerSource.rustUtil, + "Error parsing Rust index file: std not available" + ); + + return; + } + + if ( + data.pkg && + data.pkg["rust-analysis"] && + data.pkg["rust-analysis"].target && + data.pkg["rust-analysis"].target[targetTriple] && + data.pkg["rust-analysis"].target[targetTriple].available + ) { + const stdDownloadLink = + data.pkg["rust-analysis"].target[targetTriple].xz_url; + progressState = 0; + const targetLabel = `rust-analysis-${targetTriple}`; + const newTargetDirectory = joinPosix(targetDirectory, targetLabel); + const result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Pico Rust target analysis library", + cancellable: false, + }, + async progress => + downloadAndInstallArchive( + stdDownloadLink, + newTargetDirectory, + `rust-pico-${latestRelease}-analysis.tar.xz`, + "Rust Pico Analysis Library", + undefined, + undefined, + undefined, + (prog: GotProgress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + }, + `rust-analysis-${latestRelease}-${targetTriple}/${targetLabel}` + ) + ); + + if (!result) { + return undefined; + } + } else { + Logger.error( + LoggerSource.rustUtil, + "Error parsing Rust index file: analysis not available" + ); + + return; + } + + // merge all dirs up by one lvl + await mergeDirectories(targetDirectory); + + if (process.platform === "win32") { + // install portable MSVC + const result = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing MSVC", + cancellable: false, + }, + async () => installPortableMSVC() + ); + // TODO: error handling + if (!result) { + return undefined; + } + } + + // install dependencies + const cargoExecutable = joinPosix( + targetDirectory, + "bin", + "cargo" + (process.platform === "win32" ? ".exe" : "") + ); + + // TODO: add error handling + let result = await cargoInstall(cargoExecutable, "flip-link", false, {}); + if (!result) { + return undefined; + } + const hd = homedir().replaceAll("\\", "/"); + // TODO: install cmake + result = await cargoInstall(cargoExecutable, "probe-rs-tools", true, { + PATH: `${hd}/.pico-sdk/cmake/v3.28.6/bin;${hd}/.pico-sdk/rust/latest/bin;${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt`, + + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_HOST_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_TGT_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsVersion: "14.41.34120", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSDKVersion: "10.0.19041.0", + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + VCToolsInstallDir: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/`, + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSdkBinPath: `${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/`, + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + INCLUDE: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt`, + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + LIB: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64`, + }); + if (!result) { + return undefined; + } + result = await cargoInstall(cargoExecutable, "elf2uf2-rs", true, {}); + if (!result) { + return undefined; + } + + if (existingInstallation) { + void window.showInformationMessage("Rust updated successfully"); + } + + // symlink to latest + const latestPath = joinPosix(homedir(), ".pico-sdk", "rust", "latest"); + if (existsSync(latestPath)) { + rmSync(latestPath, { recursive: true, force: true }); + } + + symlinkSync(targetDirectory, latestPath, "junction"); + + return cargoExecutable; + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + "Failed to parse Rust index file or downlaod dependencies:", + unknownErrorToString(error) + ); + + return undefined; + } +} 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/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts new file mode 100644 index 00000000..d463399c --- /dev/null +++ b/src/webview/newRustProjectPanel.mts @@ -0,0 +1,742 @@ +/* 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, readFileSync } from "fs"; +import { join } from "path"; +import { unknownErrorToString } from "../utils/errorHelper.mjs"; +import { downloadAndInstallRust } from "../utils/rustUtil.mjs"; +import { cloneRepository, getGit } from "../utils/gitUtil.mjs"; + +interface SubmitMessageValue { + projectName: string; + pythonMode: number; + pythonPath: 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 MicroPico 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; + } + + // create the folder with project name in project root + + // create the project folder + const projectFolder = join(projectPath, data.projectName); + progress.report({ + message: `Creating project folder ${projectFolder}`, + increment: 10, + }); + + const gitPath = await getGit(this._settings); + + try { + await workspace.fs.createDirectory(Uri.file(projectFolder)); + + // also create a blink.py in it with a import machine + // TODO: put into const and cache template + const result = await cloneRepository( + "https://github.com/rp-rs/rp2040-project-template.git", + "main", + projectFolder, + gitPath + ); + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + `Failed to clone project template to ${projectFolder}` + ); + + return; + } + + await workspace.fs.writeFile( + Uri.file(join(projectFolder, ".pico-rs")), + new Uint8Array() + ); + await workspace.fs.writeFile( + Uri.file(join(projectFolder, ".vscode", "extensions.json")), + Buffer.from( + JSON.stringify({ + recommendations: [ + "rust-lang.rust-analyzer", + this._settings.getExtensionId(), + ], + }), + "utf-8" + ) + ); + await workspace.fs.writeFile( + Uri.file(join(projectFolder, ".vscode", "tasks.json")), + Buffer.from( + JSON.stringify({ + version: "2.0.0", + tasks: [ + { + label: "Compile Project", + type: "process", + isBuildCommand: true, + command: "${userHome}/.pico-sdk/rust/latest/bin/cargo", + args: ["build"], + group: { + kind: "build", + isDefault: true, + }, + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: "$rustc", + windows: { + command: + "${env:USERPROFILE}/.pico-sdk/rust/latest/bin/cargo.exe", + args: ["build"], + options: { + env: { + // eslint-disable-next-line @typescript-eslint/naming-convention + PATH: "${env:PATH};${userHome}/.cargo/bin;${userHome}/.pico-sdk/rust/latest/bin;${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt", + + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_HOST_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_TGT_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsVersion: "14.41.34120", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSDKVersion: "10.0.19041.0", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsInstallDir: + "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSdkBinPath: + "${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/", + // eslint-disable-next-line @typescript-eslint/naming-convention + INCLUDE: + "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt", + // eslint-disable-next-line @typescript-eslint/naming-convention + LIB: "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64", + }, + }, + }, + options: { + env: { + // eslint-disable-next-line @typescript-eslint/naming-convention + PATH: "${env:PATH}:${userHome}/.cargo/bin:${userHome}/.pico-sdk/rust/latest/bin", + }, + }, + }, + { + label: "Run", + type: "shell", + command: "${userHome}/.pico-sdk/rust/latest/bin/cargo.exe", + args: ["run", "--release"], + group: { + kind: "test", + isDefault: true, + }, + problemMatcher: "$rustc", + windows: { + command: + "${env:USERPROFILE}/.pico-sdk/rust/latest/bin/cargo.exe", + args: ["run", "--release"], + options: { + env: { + // eslint-disable-next-line @typescript-eslint/naming-convention + PATH: "${env:PATH};${userHome}/.cargo/bin;${userHome}/.pico-sdk/rust/latest/bin;${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt", + + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_HOST_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_TGT_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsVersion: "14.41.34120", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSDKVersion: "10.0.19041.0", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsInstallDir: + "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSdkBinPath: + "${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/", + // eslint-disable-next-line @typescript-eslint/naming-convention + INCLUDE: + "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt", + // eslint-disable-next-line @typescript-eslint/naming-convention + LIB: "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64", + }, + }, + }, + options: { + env: { + // eslint-disable-next-line @typescript-eslint/naming-convention + PATH: "${env:PATH}:${userHome}/.cargo/bin:${userHome}/.pico-sdk/rust/latest/bin", + }, + }, + }, + ], + }), + "utf-8" + ) + ); + + const settingsFile = join(projectFolder, ".vscode", "settings.json"); + const settingsContent = existsSync(settingsFile) + ? (JSON.parse(readFileSync(settingsFile, "utf-8")) as + | { + [key: string]: unknown; + } + | undefined) + : {}; + if (!settingsContent) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + `Failed to read settings file ${settingsFile}` + ); + + return; + } + + settingsContent["files.exclude"] = { + ...(settingsContent["files.exclude"] ?? {}), + // eslint-disable-next-line @typescript-eslint/naming-convention + ".pico-rs": true, + }; + await workspace.fs.writeFile( + Uri.file(settingsFile), + Buffer.from(JSON.stringify(settingsContent, null, 4), "utf-8") + ); + } catch { + progress.report({ + message: "Failed", + increment: 100, + }); + await window.showErrorMessage( + `Failed to create project folder ${projectFolder}` + ); + + 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"; + + this._panel.iconPath = Uri.joinPath( + this._extensionUri, + "web", + "raspberry-128.png" + ); + const html = this._getHtmlForWebview(this._panel.webview); + + if (html !== "") { + try { + this._panel.webview.html = html; + } catch (error) { + this._logger.error( + "Failed to set webview html. Webview might have been disposed. Error: ", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + + return; + } + await this._updateTheme(); + } else { + void window.showErrorMessage( + "Failed to load webview for new Rust Pico project" + ); + this.dispose(); + } + } + + private async _updateTheme(): Promise { + try { + await this._panel.webview.postMessage({ + command: "setTheme", + theme: + window.activeColorTheme.kind === ColorThemeKind.Dark || + window.activeColorTheme.kind === ColorThemeKind.HighContrast + ? "dark" + : "light", + }); + } catch (error) { + this._logger.error( + "Failed to update theme in webview. Webview might have been disposed. Error:", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + } + } + + public dispose(): void { + NewRustProjectPanel.currentPanel = undefined; + + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + + if (x) { + x.dispose(); + } + } + } + + private _getHtmlForWebview(webview: Webview): string { + const mainScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "rust", "main.js") + ); + + const mainStyleUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "main.css") + ); + + const tailwindcssScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "tailwindcss-3_3_5.js") + ); + + // images + const navHeaderSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header.svg") + ); + + const navHeaderDarkSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header-dark.svg") + ); + + // Restrict the webview to only load specific scripts + const nonce = getNonce(); + + return ` + + + + + + + + + + New Pico Rust Project + + + + + +
+
+ + +
+
+

Basic Settings

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

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

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+ + +
+
+ + + + `; + } +} diff --git a/web/rust/main.js b/web/rust/main.js new file mode 100644 index 00000000..1b5521c8 --- /dev/null +++ b/web/rust/main.js @@ -0,0 +1,130 @@ +"use strict"; + +const CMD_CHANGE_LOCATION = 'changeLocation'; +const CMD_SUBMIT = 'submit'; +const CMD_CANCEL = 'cancel'; +const CMD_SET_THEME = 'setTheme'; +const CMD_ERROR = 'error'; +const CMD_SUBMIT_DENIED = 'submitDenied'; + +var submitted = false; + +(function () { + const vscode = acquireVsCodeApi(); + + // needed so a element isn't hidden behind the navbar on scroll + const navbarOffsetHeight = document.getElementById('top-navbar').offsetHeight; + + // returns true if project name input is valid + function projectNameFormValidation(projectNameElement) { + if (typeof examples !== 'undefined') { + return true; + } + + const projectNameError = document.getElementById('inp-project-name-error'); + const projectName = projectNameElement.value; + + var invalidChars = /[\/:*?"<>| ]/; + // check for reserved names in Windows + var reservedNames = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i; + if (projectName.trim().length == 0 || invalidChars.test(projectName) || reservedNames.test(projectName)) { + projectNameError.hidden = false; + //projectNameElement.scrollIntoView({ behavior: "smooth" }); + window.scrollTo({ + top: projectNameElement.offsetTop - navbarOffsetHeight, + behavior: 'smooth' + }); + + return false; + } + + projectNameError.hidden = true; + return true; + } + + window.changeLocation = () => { + // Send a message back to the extension + vscode.postMessage({ + command: CMD_CHANGE_LOCATION, + value: null + }); + } + + window.cancelBtnClick = () => { + // close webview + vscode.postMessage({ + command: CMD_CANCEL, + value: null + }); + } + + window.submitBtnClick = () => { + /* Catch silly users who spam the submit button */ + if (submitted) { + console.error("already submitted"); + return; + } + submitted = true; + + // get all values of inputs + const projectNameElement = document.getElementById('inp-project-name'); + // if is project import then the project name element will not be rendered and does not exist in the DOM + const projectName = projectNameElement.value; + if (projectName !== undefined && !projectNameFormValidation(projectNameElement)) { + submitted = false; + return; + } + + //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); + + 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) + }); +}()); From 24d74a65ea509144b8046526d4ac8cec0e7cc90f Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:26:48 +0100 Subject: [PATCH 02/27] Some error catching and progress Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/extension.mts | 9 +++- src/utils/rustUtil.mts | 116 +++++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/extension.mts b/src/extension.mts index ef3dfab0..54fd9ee2 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -286,7 +286,14 @@ export async function activate(context: ExtensionContext): Promise { await commands.executeCommand("setContext", ContextKeys.isPicoProject, true); if (isRustProject) { - const cargo = await downloadAndInstallRust(); + const cargo = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Rust. This may take a while...", + cancellable: false, + }, + async () => downloadAndInstallRust() + ); if (!cargo) { void window.showErrorMessage("Failed to install Rust."); diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 49017f6b..aceeb53d 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -396,7 +396,12 @@ export async function downloadAndInstallRust(): Promise { const index = await downloadAndReadFile(STABLE_INDEX_DOWNLOAD_URL); if (!index) { - // TODO: undo rust download + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + return undefined; } Logger.debug(LoggerSource.rustUtil, "Downloaded Rust index file"); @@ -441,7 +446,13 @@ export async function downloadAndInstallRust(): Promise { ); if (!result) { - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } } else { Logger.error( @@ -449,6 +460,12 @@ export async function downloadAndInstallRust(): Promise { "Error parsing Rust index file: std not available" ); + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + return; } @@ -489,7 +506,13 @@ export async function downloadAndInstallRust(): Promise { ); if (!result) { - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } } else { Logger.error( @@ -497,6 +520,12 @@ export async function downloadAndInstallRust(): Promise { "Error parsing Rust index file: analysis not available" ); + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + return; } @@ -515,7 +544,13 @@ export async function downloadAndInstallRust(): Promise { ); // TODO: error handling if (!result) { - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } } @@ -533,32 +568,53 @@ export async function downloadAndInstallRust(): Promise { } const hd = homedir().replaceAll("\\", "/"); // TODO: install cmake - result = await cargoInstall(cargoExecutable, "probe-rs-tools", true, { - PATH: `${hd}/.pico-sdk/cmake/v3.28.6/bin;${hd}/.pico-sdk/rust/latest/bin;${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt`, - - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_HOST_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_TGT_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VCToolsVersion: "14.41.34120", - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSDKVersion: "10.0.19041.0", - // eslint-disable-next-line @typescript-eslint/naming-convention, max-len - VCToolsInstallDir: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/`, - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSdkBinPath: `${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/`, - // eslint-disable-next-line @typescript-eslint/naming-convention, max-len - INCLUDE: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt`, - // eslint-disable-next-line @typescript-eslint/naming-convention, max-len - LIB: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64`, - }); + result = await cargoInstall( + cargoExecutable, + "probe-rs-tools", + true, + // TODO: load cmake version dynamically and download if not present + process.platform === "win32" + ? { + PATH: `${hd}/.pico-sdk/cmake/v3.28.6/bin;${hd}/.pico-sdk/rust/latest/bin;${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt`, + + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_HOST_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VSCMD_ARG_TGT_ARCH: "x64", + // eslint-disable-next-line @typescript-eslint/naming-convention + VCToolsVersion: "14.41.34120", + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSDKVersion: "10.0.19041.0", + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + VCToolsInstallDir: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/`, + // eslint-disable-next-line @typescript-eslint/naming-convention + WindowsSdkBinPath: `${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/`, + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + INCLUDE: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt`, + // eslint-disable-next-line @typescript-eslint/naming-convention, max-len + LIB: `${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64`, + } + : // eslint-disable-next-line @typescript-eslint/naming-convention + { PATH: `${hd}/.pico-sdk/cmake/v3.28.6/bin` } + ); if (!result) { - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } result = await cargoInstall(cargoExecutable, "elf2uf2-rs", true, {}); if (!result) { - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } if (existingInstallation) { @@ -581,6 +637,12 @@ export async function downloadAndInstallRust(): Promise { unknownErrorToString(error) ); - return undefined; + try { + rmSync(targetDirectory, { recursive: true, force: true }); + } catch { + /* */ + } + + return; } } From 72a8b0933c5fb563079e117a51ffe078133406c0 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:15:32 +0100 Subject: [PATCH 03/27] Added rustup support Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/commands/compileProject.mts | 1 - src/utils/download.mts | 17 +- src/utils/githubREST.mts | 9 + src/utils/rustUtil.mts | 291 +++++++++++++++++++++++----- src/webview/newRustProjectPanel.mts | 167 +++++----------- 5 files changed, 315 insertions(+), 170 deletions(-) diff --git a/src/commands/compileProject.mts b/src/commands/compileProject.mts index 93138316..12096057 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -3,7 +3,6 @@ import { EventEmitter } from "events"; import { CommandWithResult } from "./command.mjs"; import Logger from "../logger.mjs"; import Settings, { SettingsKey } from "../settings.mjs"; -import { ContextKeys } from "../contextKeys.mjs"; import State from "../state.mjs"; import { cmakeToolsForcePicoKit } from "../utils/cmakeToolsUtil.mjs"; diff --git a/src/utils/download.mts b/src/utils/download.mts index 7fbda2f9..b4550beb 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -268,6 +268,19 @@ export async function downloadAndInstallArchive( return false; } + // TODO: find and eliminate issue why this is necesarry + if (archiveExtension.length > 6) { + archiveExtension = getArchiveExtension(archiveFileName); + if (!archiveExtension) { + Logger.error( + LoggerSource.downloader, + `Could not determine archive extension for ${archiveFileName}` + ); + + return false; + } + } + const tmpBasePath = join(tmpdir(), "pico-sdk"); await mkdir(tmpBasePath, { recursive: true }); const archiveFilePath = join(tmpBasePath, archiveFileName); @@ -609,8 +622,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, diff --git a/src/utils/githubREST.mts b/src/utils/githubREST.mts index 839baa1a..2bdfe45d 100644 --- a/src/utils/githubREST.mts +++ b/src/utils/githubREST.mts @@ -26,6 +26,7 @@ export enum GithubRepository { tools = 3, picotool = 4, rust = 5, + rsTools = 6, } /** @@ -71,6 +72,8 @@ export function ownerOfRepository(repository: GithubRepository): string { return "ninja-build"; case GithubRepository.rust: return "rust-lang"; + case GithubRepository.rsTools: + return "paulober"; } } @@ -95,6 +98,8 @@ export function repoNameOfRepository(repository: GithubRepository): string { return "picotool"; case GithubRepository.rust: return "rust"; + case GithubRepository.rsTools: + return "pico-vscode-rs-tools"; } } @@ -316,6 +321,10 @@ 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/rustUtil.mts b/src/utils/rustUtil.mts index aceeb53d..7dc90fcf 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -1,35 +1,20 @@ -import { homedir } from "os"; -import { - downloadAndInstallArchive, - downloadAndReadFile, - getScriptsRoot, -} from "./download.mjs"; -import { getRustReleases } from "./githubREST.mjs"; -import { join as joinPosix } from "path/posix"; -import { - existsSync, - mkdirSync, - readdirSync, - renameSync, - rmSync, - symlinkSync, -} from "fs"; +import { homedir, tmpdir } from "os"; +import { downloadAndInstallGithubAsset } from "./download.mjs"; +import { getRustToolsReleases, GithubRepository } from "./githubREST.mjs"; +import { mkdirSync, renameSync } from "fs"; import Logger, { LoggerSource } from "../logger.mjs"; import { unknownErrorToString } from "./errorHelper.mjs"; -import { ProgressLocation, window } from "vscode"; -import type { Progress as GotProgress } from "got"; -import { parse as parseToml } from "toml"; +import { env, ProgressLocation, Uri, window, workspace } from "vscode"; import { promisify } from "util"; import { exec } from "child_process"; import { dirname, join } from "path"; -import { copyFile, mkdir, readdir, rm, stat } from "fs/promises"; -import findPython from "./pythonHelper.mjs"; -const STABLE_INDEX_DOWNLOAD_URL = - "https://static.rust-lang.org/dist/channel-rust-stable.toml"; +/*const STABLE_INDEX_DOWNLOAD_URL = + "https://static.rust-lang.org/dist/channel-rust-stable.toml";*/ const execAsync = promisify(exec); +/* interface IndexToml { pkg?: { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -89,7 +74,7 @@ function computeDownloadLink(release: string): string { "https://static.rust-lang.org/dist" + `/rust-${release}-${arch}-${platform}.tar.xz` ); -} +}*/ export async function cargoInstall( cargoExecutable: string, @@ -120,16 +105,32 @@ export async function cargoInstall( }; } try { - const { stdout, stderr } = await execAsync(command, { - shell: process.platform === "win32" ? "powershell.exe" : undefined, - windowsHide: true, - env: { - ...customEnv, - }, - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stdout, stderr } = await execAsync( + `${command} install ${locked ? "--locked " : ""}${packageName}`, + { + windowsHide: true, + } + ); + + if (stderr) { + // TODO: find better solution + if ( + stderr.toLowerCase().includes("already exists") || + stderr.toLowerCase().includes("to your path") || + stderr.toLowerCase().includes("is already installed") || + stderr.toLowerCase().includes("yanked in registry") + ) { + Logger.debug( + LoggerSource.rustUtil, + `Cargo package '${packageName}' is already installed ` + + "or cargo bin not in PATH:", + stderr + ); + + return true; + } - // TODO: use better indication - if (stderr && !stderr.includes("to your PATH")) { Logger.error( LoggerSource.rustUtil, `Failed to install cargo command '${command}': ${stderr}` @@ -143,8 +144,13 @@ export async function cargoInstall( return true; } catch (error) { const msg = unknownErrorToString(error); - if (msg.includes("to your PATH")) { - Logger.debug( + 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 command '${command}' failed but ignoring:`, msg @@ -526,8 +532,22 @@ export async function downloadAndInstallRust(): Promise { /* */ } - return; - } +/** + * 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 { + /*try { + const rustup = process.platform === "win32" ? "rustup.exe" : "rustup"; + const cargo = process.platform === "win32" ? "cargo.exe" : "cargo"; + const commands = [ + `${rustup} target add thumbv6m-none-eabi`, + `${cargo} install flip-link`, + `${cargo} install --locked probe-rs-tools`, + `${cargo} install --locked elf2uf2-rs`, + ]; // merge all dirs up by one lvl await mergeDirectories(targetDirectory); @@ -617,9 +637,14 @@ export async function downloadAndInstallRust(): Promise { return; } - if (existingInstallation) { - void window.showInformationMessage("Rust updated successfully"); - } + // install flip-link + const flipLink = "flip-link"; + const result = await cargoInstall(flipLink, false); + if (!result) { + void window.showErrorMessage( + `Failed to install cargo package '${flipLink}'.` + + "Please check the logs." + ); // symlink to latest const latestPath = joinPosix(homedir(), ".pico-sdk", "rust", "latest"); @@ -627,14 +652,13 @@ export async function downloadAndInstallRust(): Promise { rmSync(latestPath, { recursive: true, force: true }); } - symlinkSync(targetDirectory, latestPath, "junction"); - - return cargoExecutable; - } catch (error) { - Logger.error( - LoggerSource.rustUtil, - "Failed to parse Rust index file or downlaod dependencies:", - unknownErrorToString(error) + // install probe-rs-tools + const probeRsTools = "probe-rs-tools"; + const result2 = await cargoInstall(probeRsTools, true); + if (!result2) { + void window.showErrorMessage( + `Failed to install cargo package '${probeRsTools}'.` + + "Please check the logs." ); try { @@ -643,6 +667,175 @@ export async function downloadAndInstallRust(): Promise { /* */ } - return; + // install elf2uf2-rs + const elf2uf2Rs = "elf2uf2-rs"; + const result3 = await cargoInstall(elf2uf2Rs, true); + if (!result3) { + void window.showErrorMessage( + `Failed to install cargo package '${elf2uf2Rs}'.` + + "Please check the logs." + ); + + return false; + } + + // install cargo-generate binary + const result4 = await installCargoGenerate(); + if (!result4) { + 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; +} + +export async function generateRustProject( + projectFolder: string, + name: string, + flashMethod: string +): 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="${flashMethod}"\n`, "utf-8") + ); + + // TODO: fix outside function (maybe) + let projectRoot = projectFolder; + if (projectFolder.endsWith(name)) { + projectRoot = projectFolder.slice(0, projectFolder.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; } diff --git a/src/webview/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts index d463399c..16acd81a 100644 --- a/src/webview/newRustProjectPanel.mts +++ b/src/webview/newRustProjectPanel.mts @@ -22,8 +22,10 @@ import { import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; -import { downloadAndInstallRust } from "../utils/rustUtil.mjs"; -import { cloneRepository, getGit } from "../utils/gitUtil.mjs"; +import { + downloadAndInstallRust, + generateRustProject, +} from "../utils/rustUtil.mjs"; interface SubmitMessageValue { projectName: string; @@ -295,18 +297,13 @@ export class NewRustProjectPanel { increment: 10, }); - const gitPath = await getGit(this._settings); - try { - await workspace.fs.createDirectory(Uri.file(projectFolder)); - - // also create a blink.py in it with a import machine - // TODO: put into const and cache template - const result = await cloneRepository( - "https://github.com/rp-rs/rp2040-project-template.git", - "main", + //await workspace.fs.createDirectory(Uri.file(projectFolder)); + // TODO: add flash method to ui + const result = await generateRustProject( projectFolder, - gitPath + data.projectName, + "probe-rs" ); if (!result) { progress.report({ @@ -314,7 +311,7 @@ export class NewRustProjectPanel { increment: 100, }); void window.showErrorMessage( - `Failed to clone project template to ${projectFolder}` + `Failed to create project folder ${projectFolder}` ); return; @@ -327,124 +324,58 @@ export class NewRustProjectPanel { await workspace.fs.writeFile( Uri.file(join(projectFolder, ".vscode", "extensions.json")), Buffer.from( - JSON.stringify({ - recommendations: [ - "rust-lang.rust-analyzer", - this._settings.getExtensionId(), - ], - }), + JSON.stringify( + { + recommendations: [ + "rust-lang.rust-analyzer", + this._settings.getExtensionId(), + ], + }, + undefined, + 4 + ), "utf-8" ) ); await workspace.fs.writeFile( Uri.file(join(projectFolder, ".vscode", "tasks.json")), Buffer.from( - JSON.stringify({ - version: "2.0.0", - tasks: [ - { - label: "Compile Project", - type: "process", - isBuildCommand: true, - command: "${userHome}/.pico-sdk/rust/latest/bin/cargo", - args: ["build"], - group: { - kind: "build", - isDefault: true, - }, - presentation: { - reveal: "always", - panel: "dedicated", - }, - problemMatcher: "$rustc", - windows: { - command: - "${env:USERPROFILE}/.pico-sdk/rust/latest/bin/cargo.exe", + JSON.stringify( + { + version: "2.0.0", + tasks: [ + { + label: "Compile Project", + type: "process", + isBuildCommand: true, + command: "cargo", args: ["build"], - options: { - env: { - // eslint-disable-next-line @typescript-eslint/naming-convention - PATH: "${env:PATH};${userHome}/.cargo/bin;${userHome}/.pico-sdk/rust/latest/bin;${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt", - - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_HOST_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_TGT_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VCToolsVersion: "14.41.34120", - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSDKVersion: "10.0.19041.0", - // eslint-disable-next-line @typescript-eslint/naming-convention - VCToolsInstallDir: - "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/", - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSdkBinPath: - "${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/", - // eslint-disable-next-line @typescript-eslint/naming-convention - INCLUDE: - "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt", - // eslint-disable-next-line @typescript-eslint/naming-convention - LIB: "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64", - }, + group: { + kind: "build", + isDefault: true, }, - }, - options: { - env: { - // eslint-disable-next-line @typescript-eslint/naming-convention - PATH: "${env:PATH}:${userHome}/.cargo/bin:${userHome}/.pico-sdk/rust/latest/bin", + presentation: { + reveal: "always", + panel: "dedicated", }, + problemMatcher: "$rustc", }, - }, - { - label: "Run", - type: "shell", - command: "${userHome}/.pico-sdk/rust/latest/bin/cargo.exe", - args: ["run", "--release"], - group: { - kind: "test", - isDefault: true, - }, - problemMatcher: "$rustc", - windows: { - command: - "${env:USERPROFILE}/.pico-sdk/rust/latest/bin/cargo.exe", + { + label: "Run Project", + type: "shell", + command: "cargo", args: ["run", "--release"], - options: { - env: { - // eslint-disable-next-line @typescript-eslint/naming-convention - PATH: "${env:PATH};${userHome}/.cargo/bin;${userHome}/.pico-sdk/rust/latest/bin;${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt", - - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_HOST_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VSCMD_ARG_TGT_ARCH: "x64", - // eslint-disable-next-line @typescript-eslint/naming-convention - VCToolsVersion: "14.41.34120", - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSDKVersion: "10.0.19041.0", - // eslint-disable-next-line @typescript-eslint/naming-convention - VCToolsInstallDir: - "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/", - // eslint-disable-next-line @typescript-eslint/naming-convention - WindowsSdkBinPath: - "${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/bin/", - // eslint-disable-next-line @typescript-eslint/naming-convention - INCLUDE: - "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/include;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/ucrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/shared;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/um;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/winrt;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Include/10.0.19041.0/cppwinrt", - // eslint-disable-next-line @typescript-eslint/naming-convention - LIB: "${userHome}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/lib/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64;${userHome}/.pico-sdk/msvc/latest/Windows Kits/10/Lib/10.0.19041.0/um/x64", - }, - }, - }, - options: { - env: { - // eslint-disable-next-line @typescript-eslint/naming-convention - PATH: "${env:PATH}:${userHome}/.cargo/bin:${userHome}/.pico-sdk/rust/latest/bin", + group: { + kind: "test", + isDefault: true, }, + problemMatcher: "$rustc", }, - }, - ], - }), + ], + }, + undefined, + 4 + ), "utf-8" ) ); From b3991f6e96ded3c2404babdcddc1c518f84cb874 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:10:18 +0100 Subject: [PATCH 04/27] WIP Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- package.json | 16 +- src/commands/conditionalDebugging.mts | 23 +- src/commands/getPaths.mts | 42 +- src/commands/launchTargetPath.mts | 80 +- src/commands/switchBoard.mts | 143 +- src/extension.mts | 35 +- src/logger.mts | 1 + src/ui.mts | 25 +- src/utils/projectGeneration/projectRust.mts | 1338 +++++++++++++++++++ src/utils/projectGeneration/tomlUtil.mts | 95 ++ src/utils/rustUtil.mts | 141 +- src/webview/newRustProjectPanel.mts | 162 +-- web/mpy/main.js | 1 - web/rust/main.js | 30 +- 14 files changed, 1890 insertions(+), 242 deletions(-) create mode 100644 src/utils/projectGeneration/projectRust.mts create mode 100644 src/utils/projectGeneration/tomlUtil.mts diff --git a/package.json b/package.json index 65fa2ef0..db150cee 100644 --- a/package.json +++ b/package.json @@ -94,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", @@ -148,6 +154,12 @@ "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.compileProject", "title": "Compile Pico Project", @@ -158,7 +170,7 @@ "command": "raspberry-pi-pico.runProject", "title": "Run Pico Project (USB)", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" + "enablement": "raspberry-pi-pico.isPicoProject" }, { "command": "raspberry-pi-pico.clearGithubApiCache", @@ -169,7 +181,7 @@ "command": "raspberry-pi-pico.conditionalDebugging", "title": "Conditional Debugging", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !inQuickOpen && !raspberry-pi-pico.isRustProject" + "enablement": "raspberry-pi-pico.isPicoProject && !inQuickOpen" }, { "command": "raspberry-pi-pico.debugLayout", diff --git a/src/commands/conditionalDebugging.mts b/src/commands/conditionalDebugging.mts index d1148e8d..ddc5165d 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, "rp2040-project"); + + return; + } + await commands.executeCommand("workbench.action.debug.selectandstart"); } } diff --git a/src/commands/getPaths.mts b/src/commands/getPaths.mts index cfbc11d5..a470eb14 100644 --- a/src/commands/getPaths.mts +++ b/src/commands/getPaths.mts @@ -8,14 +8,17 @@ import { } from "../utils/cmakeUtil.mjs"; import { join } from "path"; import { + buildOpenOCDPath, buildPicotoolPath, 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 { openOCDVersion } from "../webview/newProjectPanel.mjs"; export class GetPythonPathCommand extends CommandWithResult { constructor() { @@ -143,7 +146,8 @@ export class GetCompilerPathCommand extends CommandWithResult { } return join( - buildToolchainPath(toolchainVersion), "bin", + buildToolchainPath(toolchainVersion), + "bin", triple + `-gcc${process.platform === "win32" ? ".exe" : ""}` ); } @@ -301,8 +305,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 +349,35 @@ 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(openOCDVersion); + + if (result === null || !result) { + this.running = false; + + return undefined; + } + + this.running = false; + + return buildOpenOCDPath(openOCDVersion); + } +} diff --git a/src/commands/launchTargetPath.mts b/src/commands/launchTargetPath.mts index 6cc83e71..2568bf2b 100644 --- a/src/commands/launchTargetPath.mts +++ b/src/commands/launchTargetPath.mts @@ -3,11 +3,16 @@ import { CommandWithResult } from "./command.mjs"; import { commands, window, workspace } from "vscode"; import { join } from "path"; import Settings, { SettingsKey } from "../settings.mjs"; +import State from "../state.mjs"; +import { parse as parseToml } from "toml"; +import { join as joinPosix } from "path/posix"; import { cmakeToolsForcePicoKit } from "../utils/cmakeToolsUtil.mjs"; export default class LaunchTargetPathCommand extends CommandWithResult { + public static readonly id = "launchTargetPath"; + constructor() { - super("launchTargetPath"); + super(LaunchTargetPathCommand.id); } private async readProjectNameFromCMakeLists( @@ -62,6 +67,33 @@ export default class LaunchTargetPathCommand extends CommandWithResult { return ""; } + const isRustProject = State.getInstance().isRustProject; + + if (isRustProject) { + const cargoTomlPath = join( + workspace.workspaceFolders[0].uri.fsPath, + "Cargo.toml" + ); + const contents = readFileSync(cargoTomlPath, "utf-8"); + const cargoToml = (await parseToml(contents)) as + | { + package?: { name?: string }; + } + | undefined; + + if (cargoToml?.package?.name) { + return joinPosix( + workspace.workspaceFolders[0].uri.fsPath.replaceAll("\\", "/"), + "target", + "thumbv6m-none-eabi", + "debug", + cargoToml.package.name + ); + } + + return ""; + } + const settings = Settings.getInstance(); if ( settings !== undefined && @@ -106,3 +138,49 @@ export default class LaunchTargetPathCommand extends CommandWithResult { ); } } + +export class LaunchTargetPathReleaseCommand extends CommandWithResult { + public static readonly id = "launchTargetPathRelease"; + + constructor() { + super(LaunchTargetPathReleaseCommand.id); + } + + async execute(): Promise { + if ( + workspace.workspaceFolders === undefined || + workspace.workspaceFolders.length === 0 + ) { + return ""; + } + + const isRustProject = State.getInstance().isRustProject; + + if (!isRustProject) { + return ""; + } + + const cargoTomlPath = join( + workspace.workspaceFolders[0].uri.fsPath, + "Cargo.toml" + ); + const contents = readFileSync(cargoTomlPath, "utf-8"); + const cargoToml = (await parseToml(contents)) as + | { + package?: { name?: string }; + } + | undefined; + + if (cargoToml?.package?.name) { + return joinPosix( + workspace.workspaceFolders[0].uri.fsPath.replaceAll("\\", "/"), + "target", + "thumbv6m-none-eabi", + "release", + cargoToml.package.name + ); + } + + return ""; + } +} diff --git a/src/commands/switchBoard.mts b/src/commands/switchBoard.mts index 30d4fa16..6579dbc1 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 { - buildSDKPath, downloadAndInstallToolchain + buildSDKPath, + downloadAndInstallToolchain, } from "../utils/download.mjs"; import { cmakeGetSelectedToolchainAndSDKVersions, @@ -21,8 +26,14 @@ 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"; +import { writeFile } from "fs/promises"; +import { parse as parseToml } from "toml"; +import { writeTomlFile } from "../utils/projectGeneration/tomlUtil.mjs"; export default class SwitchBoardCommand extends Command { + private _logger: Logger = new Logger("SwitchBoardCommand"); private _versionBundlesLoader: VersionBundlesLoader; public static readonly id = "switchBoard"; @@ -32,8 +43,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 +124,6 @@ export default class SwitchBoardCommand extends Command { }); if (board === undefined) { - return board; } @@ -120,7 +131,6 @@ export default class SwitchBoardCommand extends Command { const data = readFileSync(boardFiles[board]) if (data.includes("rp2040")) { - return [board, false]; } @@ -129,7 +139,6 @@ export default class SwitchBoardCommand extends Command { }); if (useRiscV === undefined) { - return undefined; } @@ -138,15 +147,116 @@ 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: "Select chip", + } + ); + + if (board === undefined) { + return undefined; + } + + const target = + board === "rp2350-RISCV" + ? "riscv32imac-unknown-none-elf" + : board === "rp2350" + ? "thumbv8m.main-none-eabihf" + : "thumbv6m-none-eabi"; + + // check if .cargo/config.toml already contains a line starting with + // target = "${target}" and if no replace target = "..." with it with the new target + + try { + const cargoConfigPath = join( + workspaceFolder.uri.fsPath, + ".cargo", + "config.toml" + ); + + const contents = readFileSync(cargoConfigPath, "utf-8"); + + const newContents = contents.replace( + /target = ".*"/, + `target = "${target}"` + ); + + if (newContents === contents) { + return; + } + + // write new contents to file + await writeFile(cargoConfigPath, newContents); + + const cargoToml = (await parseToml(contents)) as { + features?: { default?: string[] }; + }; + + let features = cargoToml.features?.default ?? []; + + switch (board) { + case "rp2040": + features.push("rp2040"); + // remove all other features rp2350 and rp2350-riscv + features = features.filter( + f => f !== "rp2350" && f !== "rp2350-riscv" + ); + break; + case "rp2350": + features.push("rp2350"); + // remove all other features rp2040 and rp2350-riscv + features = features.filter( + f => f !== "rp2040" && f !== "rp2350-riscv" + ); + break; + case "rp2350-RISCV": + features.push("rp2350-riscv"); + // remove all other features rp2040 and rp2350 + features = features.filter(f => f !== "rp2040" && f !== "rp2350"); + break; + } + + if (cargoToml.features) { + cargoToml.features.default = features; + } else { + // not necessary becuase your project is broken at this point + cargoToml.features = { default: features }; + } + + await writeTomlFile(cargoConfigPath, cargoToml); + } catch (error) { + this._logger.error( + "Failed to update .cargo/config.toml", + unknownErrorToString(error) + ); + + void window.showErrorMessage( + "Failed to update Cargo.toml and " + + ".cargo/config.toml - cannot update chip" + ); + + return; + } + + return; + } + const versions = await cmakeGetSelectedToolchainAndSDKVersions( workspaceFolder.uri ); @@ -205,22 +315,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 +361,7 @@ export default class SwitchBoardCommand extends Command { } } } - ) + ); } const success = await cmakeUpdateBoard(workspaceFolder.uri, board); diff --git a/src/extension.mts b/src/extension.mts index 54fd9ee2..d015b0e2 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -33,7 +33,9 @@ import { existsSync, readFileSync } from "fs"; import { basename, join } from "path"; import CompileProjectCommand from "./commands/compileProject.mjs"; import RunProjectCommand from "./commands/runProject.mjs"; -import LaunchTargetPathCommand from "./commands/launchTargetPath.mjs"; +import LaunchTargetPathCommand, { + LaunchTargetPathReleaseCommand, +} from "./commands/launchTargetPath.mjs"; import { GetPythonPathCommand, GetEnvPathCommand, @@ -44,6 +46,7 @@ import { GetTargetCommand, GetChipUppercaseCommand, GetPicotoolPathCommand, + GetOpenOCDRootCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -83,9 +86,13 @@ import FlashProjectSWDCommand from "./commands/flashProjectSwd.mjs"; import { NewMicroPythonProjectPanel } from "./webview/newMicroPythonProjectPanel.mjs"; import type { Progress as GotProgress } from "got"; import findPython, { showPythonNotFoundError } from "./utils/pythonHelper.mjs"; -import { downloadAndInstallRust } from "./utils/rustUtil.mjs"; +import { + chipFromCargoToml, + downloadAndInstallRust, +} from "./utils/rustUtil.mjs"; import State from "./state.mjs"; import { cmakeToolsForcePicoKit } from "./utils/cmakeToolsUtil.mjs"; +import { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -112,6 +119,7 @@ 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(), @@ -121,6 +129,7 @@ export async function activate(context: ExtensionContext): Promise { new GetChipUppercaseCommand(), new GetTargetCommand(), new GetPicotoolPathCommand(), + new GetOpenOCDRootCommand(), new CompileProjectCommand(), new RunProjectCommand(), new FlashProjectSWDCommand(), @@ -172,6 +181,17 @@ export async function activate(context: ExtensionContext): Promise { }) ); + context.subscriptions.push( + window.registerWebviewPanelSerializer(NewRustProjectPanel.viewType, { + // eslint-disable-next-line @typescript-eslint/require-await + async deserializeWebviewPanel(webviewPanel: WebviewPanel): Promise { + // Reset the webview options so we use latest uri for `localResourceRoots`. + webviewPanel.webview.options = getWebviewOptions(context.extensionUri); + NewRustProjectPanel.revive(webviewPanel, context.extensionUri); + }, + }) + ); + context.subscriptions.push( window.registerTreeDataProvider( PicoProjectActivityBar.viewType, @@ -200,11 +220,11 @@ export async function activate(context: ExtensionContext): Promise { return; } - /*void commands.executeCommand( + void commands.executeCommand( "setContext", ContextKeys.isRustProject, isRustProject - );*/ + ); State.getInstance().isRustProject = isRustProject; if (!isRustProject) { @@ -302,6 +322,13 @@ export async function activate(context: ExtensionContext): Promise { ui.showStatusBarItems(isRustProject); + const chip = await chipFromCargoToml(); + if (chip !== null) { + ui.updateBoard(chip); + } else { + ui.updateBoard("N/A"); + } + return; } diff --git a/src/logger.mts b/src/logger.mts index 5c10d8ef..c88860f2 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -43,6 +43,7 @@ export enum LoggerSource { gitUtil = "gitUtil", vscodeConfigUtil = "vscodeConfigUtil", rustUtil = "rustUtil", + projectRust = "projectRust", } /** diff --git a/src/ui.mts b/src/ui.mts index 92db3762..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", @@ -12,6 +13,7 @@ enum StatusBarItemKey { const STATUS_BAR_ITEMS: { [key: string]: { text: string; + rustText?: string; command: string; tooltip: string; rustSupport: boolean; @@ -39,9 +41,10 @@ const STATUS_BAR_ITEMS: { }, [StatusBarItemKey.picoBoardQuickPick]: { text: "Board: ", + rustText: "Chip: ", command: "raspberry-pi-pico.switchBoard", - tooltip: "Select Board", - rustSupport: false, + tooltip: "Select Chip", + rustSupport: true, }, }; @@ -80,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/projectGeneration/projectRust.mts b/src/utils/projectGeneration/projectRust.mts new file mode 100644 index 00000000..7e23d481 --- /dev/null +++ b/src/utils/projectGeneration/projectRust.mts @@ -0,0 +1,1338 @@ +/* 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 { promisify } from "util"; +import { exec } from "child_process"; +import { + GetOpenOCDRootCommand, + GetPicotoolPathCommand, +} from "../../commands/getPaths.mjs"; +import { extensionName } from "../../commands/command.mjs"; +import { commands, window } from "vscode"; +import { getSDKReleases, SDK_REPOSITORY_URL } from "../githubREST.mjs"; +import { downloadAndInstallSDK } from "../download.mjs"; + +const execAsync = promisify(exec); + +async function generateVSCodeConfig( + projectRoot: string, + picoSDKVersion: string +): Promise { + 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 (Cortex-Debug)", + cwd: `\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/scripts`, + executable: "${command:raspberry-pi-pico.launchTargetPath}", + request: "launch", + type: "cortex-debug", + servertype: "openocd", + serverpath: `\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/openocd.exe`, + gdbPath: "${command:raspberry-pi-pico.getGDBPath}", + device: "${command:raspberry-pi-pico.getChipUppercase}", + configFiles: [ + "interface/cmsis-dap.cfg", + "target/${command:raspberry-pi-pico.getTarget}.cfg", + ], + svdFile: `\${userHome}/.pico-sdk/sdk/${picoSDKVersion}/src/\${command:raspberry-pi-pico.getChip}/hardware_regs/\${command:raspberry-pi-pico.getChipUppercase}.svd`, + runToEntryPoint: "main", + // Fix for no_flash binaries, where monitor reset halt doesn't do what is expected + // Also works fine for flash binaries + overrideLaunchCommands: [ + "monitor reset init", + 'load "${command:raspberry-pi-pico.launchTargetPath}"', + ], + openOCDLaunchCommands: ["adapter speed 5000"], + }, + ], + }; + + const settings = { + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.checkOnSave.allTargets": false, + "editor.formatOnSave": true, + "files.exclude": { + ".pico-rs": true, + }, + }; + + const tasks = { + version: "2.0.0", + tasks: [ + { + label: "Compile Project", + type: "process", + isBuildCommand: true, + command: "cargo", + args: ["build", "--release"], + group: { + kind: "build", + isDefault: true, + }, + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: "$rustc", + options: { + env: { + PICOTOOL_PATH: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + }, + }, + }, + { + label: "Run Project", + type: "process", + dependsOn: "Compile Project", + command: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + args: [ + "load", + "-x", + "${command:raspberry-pi-pico.launchTargetPath}", + "-t", + "elf", + "-f", + ], + 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 initGit(projectRoot: string): Promise { + try { + // TODO: timeouts + await execAsync("git init", { + cwd: projectRoot, + }); + await execAsync( + "git submodule add https://github.com/rp-rs/rp-hal.git rp-hal", + { + cwd: projectRoot, + } + ); + await execAsync("git submodule update --init --recursive", { + cwd: projectRoot, + }); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to initialize git", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateMainRs(projectRoot: string): Promise { + const mainRs = `//! # GPIO 'Blinky' Example +//! +//! This application demonstrates how to control a GPIO pin on the rp235x. +//! +//! It may need to be adapted to your particular board layout and/or pin assignment. +//! +//! See the \`Cargo.toml\` file for Copyright and license details. + +#![no_std] +#![no_main] + +use defmt::*; +use defmt_rtt as _; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::OutputPin; +#[cfg(feature = "rp2350")] +use panic_halt as _; +#[cfg(feature = "rp2040")] +use panic_probe as _; + +// Alias for our HAL crate +use hal::entry; + +#[cfg(feature = "rp2350")] +use rp235x_hal as hal; + +#[cfg(feature = "rp2040")] +use rp2040_hal as hal; + +// use bsp::entry; +// use bsp::hal; +// use rp_pico as bsp; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +/// Note: This boot block is not necessary when using a rp-hal based BSP +/// as the BSPs already perform this step. +#[link_section = ".boot2"] +#[used] +#[cfg(feature = "rp2040")] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; + +//\`target_abi\`, \`target_arch\`, \`target_endian\`, +//\`target_env\`, \`target_family\`, \`target_feature\`, +//\`target_has_atomic\`, \`target_has_atomic_equal_alignment\`, +//\`target_has_atomic_load_store\`, \`target_os\`, +//\`target_pointer_width\`, \`target_thread_local\`, +//\`target_vendor\` +/// Tell the Boot ROM about our application +#[link_section = ".start_block"] +#[used] +#[cfg(feature = "rp2350")] +pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe(); + +/// External high-speed crystal on the Raspberry Pi Pico 2 board is 12 MHz. +/// Adjust if your board has a different frequency +const XTAL_FREQ_HZ: u32 = 12_000_000u32; + +/// Entry point to our bare-metal application. +/// +/// The \`#[hal::entry]\` macro ensures the Cortex-M start-up code calls this function +/// as soon as all global variables and the spinlock are initialised. +/// +/// The function configures the rp235x peripherals, then toggles a GPIO pin in +/// an infinite loop. If there is an LED connected to that pin, it will blink. +#[entry] +fn main() -> ! { + info!("Program start"); + // Grab our singleton objects + let mut pac = hal::pac::Peripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + let clocks = hal::clocks::init_clocks_and_plls( + XTAL_FREQ_HZ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .unwrap(); + + #[cfg(feature = "rp2040")] + let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); + + #[cfg(feature = "rp2350")] + let mut timer = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + + // Set the pins to their default state + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Configure GPIO25 as an output + let mut led_pin = pins.gpio25.into_push_pull_output(); + loop { + info!("on!"); + led_pin.set_high().unwrap(); + timer.delay_ms(200); + info!("off!"); + led_pin.set_low().unwrap(); + timer.delay_ms(200); + } +} + +/// Program metadata for \`picotool info\` +#[link_section = ".bi_entries"] +#[used] +pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 5] = [ + hal::binary_info::rp_cargo_bin_name!(), + hal::binary_info::rp_cargo_version!(), + hal::binary_info::rp_program_description!(c"Blinky Example"), + hal::binary_info::rp_cargo_homepage_url!(), + hal::binary_info::rp_program_build_attribute!(), +]; + +// End of file +`; + + try { + await mkdir(join(projectRoot, "src")); + await writeFile(join(projectRoot, "src", "main.rs"), mainRs); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write main.rs file", + unknownErrorToString(error) + ); + + return false; + } +} + +export enum FlashMethod { + openOCD, + picotool, +} + +/* +interface CargoTomlProfile { + "codegen-units": number; + debug: number | boolean; + "debug-assertions": boolean; + incremental?: boolean; + lto?: string; + "opt-level": number; + "overflow-checks"?: boolean; +}*/ + +interface CargoTomlDependencies { + [key: string]: + | string + | TomlInlineObject<{ + optional?: boolean; + version: string; + features?: string[]; + }>; +} + +interface CargoToml { + package: { + edition: string; + name: string; + version: string; + license: string; + }; + + 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; + }; + }; + + features: { + default?: string[]; + rp2040: string[]; + rp2350: string[]; + "rp2350-riscv": string[]; + }; +} + +async function generateCargoToml( + projectRoot: string, + projectName: string +): Promise { + const obj: CargoToml = { + package: { + edition: "2021", + name: projectName, + version: "0.1.0", + license: "MIT or Apache-2.0", + }, + dependencies: { + "cortex-m": "0.7", + "cortex-m-rt": "0.7", + "embedded-hal": "1.0.0", + defmt: "0.3", + "defmt-rtt": "0.4", + "rp235x-hal": new TomlInlineObject({ + optional: true, + path: "./rp-hal/rp235x-hal", + version: "0.2.0", + features: ["rt", "critical-section-impl"], + }), + "rp2040-hal": new TomlInlineObject({ + optional: true, + path: "./rp-hal/rp2040-hal", + version: "0.10", + features: ["rt", "critical-section-impl"], + }), + "rp2040-boot2": new TomlInlineObject({ + optional: true, + version: "0.2", + }), + }, + target: { + "'cfg( target_arch = \"arm\" )'": { + dependencies: { + "panic-probe": new TomlInlineObject({ + version: "0.3", + features: ["print-defmt"], + }), + }, + }, + "'cfg( target_arch = \"riscv32\" )'": { + dependencies: { + "panic-halt": new TomlInlineObject({ + version: "0.2", + }), + }, + }, + }, + + features: { + default: ["rp2040"], + rp2040: ["rp2040-hal", "rp2040-boot2"], + rp2350: ["rp235x-hal"], + "rp2350-riscv": ["rp2350"], + }, + }; + + // write to file + try { + await writeTomlFile(join(projectRoot, "Cargo.toml"), obj); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write Cargo.toml file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateMemoryLayouts(projectRoot: string): Promise { + const rp2040X = `MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + /* + * Here we assume you have 2048 KiB of Flash. This is what the Pi Pico + * has, but your board may have more or less Flash and you should adjust + * this value to suit. + */ + FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + /* + * RAM consists of 4 banks, SRAM0-SRAM3, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 256K + /* + * RAM banks 4 and 5 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20040000, LENGTH = 4k + SRAM5 : ORIGIN = 0x20041000, LENGTH = 4k + + /* SRAM banks 0-3 can also be accessed directly. However, those ranges + alias with the RAM mapping, above. So don't use them at the same time! + SRAM0 : ORIGIN = 0x21000000, LENGTH = 64k + SRAM1 : ORIGIN = 0x21010000, LENGTH = 64k + SRAM2 : ORIGIN = 0x21020000, LENGTH = 64k + SRAM3 : ORIGIN = 0x21030000, LENGTH = 64k + */ +} + +EXTERN(BOOT2_FIRMWARE) + +SECTIONS { + /* ### Boot loader + * + * An executable block of code which sets up the QSPI interface for + * 'Execute-In-Place' (or XIP) mode. Also sends chip-specific commands to + * the external flash chip. + * + * Must go at the start of external flash, where the Boot ROM expects it. + */ + .boot2 ORIGIN(BOOT2) : + { + KEEP(*(.boot2)); + } > BOOT2 +} INSERT BEFORE .text; + +SECTIONS { + /* ### Boot ROM info + * + * Goes after .vector_table, to keep it in the first 512 bytes of flash, + * where picotool can find it + */ + .boot_info : ALIGN(4) + { + KEEP(*(.boot_info)); + } > FLASH + +} INSERT AFTER .vector_table; + +/* move .text to start /after/ the boot info */ +_stext = ADDR(.boot_info) + SIZEOF(.boot_info); + +SECTIONS { + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH +} INSERT AFTER .text; +`; + + const rp2350X = `MEMORY { + /* + * The RP2350 has either external or internal flash. + * + * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. + */ + FLASH : ORIGIN = 0x10000000, LENGTH = 2048K + /* + * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 512K + /* + * RAM banks 8 and 9 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K + SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K + } + + SECTIONS { + /* ### Boot ROM info + * + * Goes after .vector_table, to keep it in the first 4K of flash + * where the Boot ROM (and picotool) can find it + */ + .start_block : ALIGN(4) + { + __start_block_addr = .; + KEEP(*(.start_block)); + } > FLASH + + } INSERT AFTER .vector_table; + + /* move .text to start /after/ the boot info */ + _stext = ADDR(.start_block) + SIZEOF(.start_block); + + SECTIONS { + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH + } INSERT AFTER .text; + + SECTIONS { + /* ### Boot ROM extra info + * + * Goes after everything in our program, so it can contain a signature. + */ + .end_block : ALIGN(4) + { + __end_block_addr = .; + KEEP(*(.end_block)); + } > FLASH + + } INSERT AFTER .uninit; + + PROVIDE(start_to_end = __end_block_addr - __start_block_addr); + PROVIDE(end_to_start = __start_block_addr - __end_block_addr); + `; + + const rp2350RiscvX = `MEMORY { + /* + * The RP2350 has either external or internal flash. + * + * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. + */ + FLASH : ORIGIN = 0x10000000, LENGTH = 2048K + /* + * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. + * This is usually good for performance, as it distributes load on + * those banks evenly. + */ + RAM : ORIGIN = 0x20000000, LENGTH = 512K + /* + * RAM banks 8 and 9 use a direct mapping. They can be used to have + * memory areas dedicated for some specific job, improving predictability + * of access times. + * Example: Separate stacks for core0 and core1. + */ + SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K + SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K +} + +/* # Developer notes + +- Symbols that start with a double underscore (__) are considered "private" + +- Symbols that start with a single underscore (_) are considered "semi-public"; they can be + overridden in a user linker script, but should not be referred from user code (e.g. \`extern "C" { + static mut _heap_size }\`). + +- \`EXTERN\` forces the linker to keep a symbol in the final binary. We use this to make sure a + symbol is not dropped if it appears in or near the front of the linker arguments and "it's not + needed" by any of the preceding objects (linker arguments) + +- \`PROVIDE\` is used to provide default values that can be overridden by a user linker script + +- On alignment: it's important for correctness that the VMA boundaries of both .bss and .data *and* + the LMA of .data are all \`32\`-byte aligned. These alignments are assumed by the RAM + initialization routine. There's also a second benefit: \`32\`-byte aligned boundaries + means that you won't see "Address (..) is out of bounds" in the disassembly produced by \`objdump\`. +*/ + +PROVIDE(_stext = ORIGIN(FLASH)); +PROVIDE(_stack_start = ORIGIN(RAM) + LENGTH(RAM)); +PROVIDE(_max_hart_id = 0); +PROVIDE(_hart_stack_size = 2K); +PROVIDE(_heap_size = 0); + +PROVIDE(InstructionMisaligned = ExceptionHandler); +PROVIDE(InstructionFault = ExceptionHandler); +PROVIDE(IllegalInstruction = ExceptionHandler); +PROVIDE(Breakpoint = ExceptionHandler); +PROVIDE(LoadMisaligned = ExceptionHandler); +PROVIDE(LoadFault = ExceptionHandler); +PROVIDE(StoreMisaligned = ExceptionHandler); +PROVIDE(StoreFault = ExceptionHandler); +PROVIDE(UserEnvCall = ExceptionHandler); +PROVIDE(SupervisorEnvCall = ExceptionHandler); +PROVIDE(MachineEnvCall = ExceptionHandler); +PROVIDE(InstructionPageFault = ExceptionHandler); +PROVIDE(LoadPageFault = ExceptionHandler); +PROVIDE(StorePageFault = ExceptionHandler); + +PROVIDE(SupervisorSoft = DefaultHandler); +PROVIDE(MachineSoft = DefaultHandler); +PROVIDE(SupervisorTimer = DefaultHandler); +PROVIDE(MachineTimer = DefaultHandler); +PROVIDE(SupervisorExternal = DefaultHandler); +PROVIDE(MachineExternal = DefaultHandler); + +PROVIDE(DefaultHandler = DefaultInterruptHandler); +PROVIDE(ExceptionHandler = DefaultExceptionHandler); + +/* # Pre-initialization function */ +/* If the user overrides this using the \`#[pre_init]\` attribute or by creating a \`__pre_init\` function, + then the function this points to will be called before the RAM is initialized. */ +PROVIDE(__pre_init = default_pre_init); + +/* A PAC/HAL defined routine that should initialize custom interrupt controller if needed. */ +PROVIDE(_setup_interrupts = default_setup_interrupts); + +/* # Multi-processing hook function + fn _mp_hook() -> bool; + + This function is called from all the harts and must return true only for one hart, + which will perform memory initialization. For other harts it must return false + and implement wake-up in platform-dependent way (e.g. after waiting for a user interrupt). +*/ +PROVIDE(_mp_hook = default_mp_hook); + +/* # Start trap function override + By default uses the riscv crates default trap handler + but by providing the \`_start_trap\` symbol external crates can override. +*/ +PROVIDE(_start_trap = default_start_trap); + +SECTIONS +{ + .text.dummy (NOLOAD) : + { + /* This section is intended to make _stext address work */ + . = ABSOLUTE(_stext); + } > FLASH + + .text _stext : + { + /* Put reset handler first in .text section so it ends up as the entry */ + /* point of the program. */ + KEEP(*(.init)); + KEEP(*(.init.rust)); + . = ALIGN(4); + __start_block_addr = .; + KEEP(*(.start_block)); + . = ALIGN(4); + *(.trap); + *(.trap.rust); + *(.text.abort); + *(.text .text.*); + . = ALIGN(4); + } > FLASH + + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH + + .rodata : ALIGN(4) + { + *(.srodata .srodata.*); + *(.rodata .rodata.*); + + /* 4-byte align the end (VMA) of this section. + This is required by LLD to ensure the LMA of the following .data + section will have the correct alignment. */ + . = ALIGN(4); + } > FLASH + + .data : ALIGN(32) + { + _sidata = LOADADDR(.data); + __sidata = LOADADDR(.data); + _sdata = .; + __sdata = .; + /* Must be called __global_pointer$ for linker relaxations to work. */ + PROVIDE(__global_pointer$ = . + 0x800); + *(.sdata .sdata.* .sdata2 .sdata2.*); + *(.data .data.*); + . = ALIGN(32); + _edata = .; + __edata = .; + } > RAM AT > FLASH + + .bss (NOLOAD) : ALIGN(32) + { + _sbss = .; + *(.sbss .sbss.* .bss .bss.*); + . = ALIGN(32); + _ebss = .; + } > RAM + + .end_block : ALIGN(4) + { + __end_block_addr = .; + KEEP(*(.end_block)); + } > FLASH + + /* fictitious region that represents the memory available for the heap */ + .heap (NOLOAD) : + { + _sheap = .; + . += _heap_size; + . = ALIGN(4); + _eheap = .; + } > RAM + + /* fictitious region that represents the memory available for the stack */ + .stack (NOLOAD) : + { + _estack = .; + . = ABSOLUTE(_stack_start); + _sstack = .; + } > RAM + + /* fake output .got section */ + /* Dynamic relocations are unsupported. This section is only used to detect + relocatable code in the input files and raise an error if relocatable code + is found */ + .got (INFO) : + { + KEEP(*(.got .got.*)); + } + + .eh_frame (INFO) : { KEEP(*(.eh_frame)) } + .eh_frame_hdr (INFO) : { *(.eh_frame_hdr) } +} + +PROVIDE(start_to_end = __end_block_addr - __start_block_addr); +PROVIDE(end_to_start = __start_block_addr - __end_block_addr); + + +/* Do not exceed this mark in the error messages above | */ +ASSERT(ORIGIN(FLASH) % 4 == 0, " +ERROR(riscv-rt): the start of the FLASH must be 4-byte aligned"); + +ASSERT(ORIGIN(RAM) % 32 == 0, " +ERROR(riscv-rt): the start of the RAM must be 32-byte aligned"); + +ASSERT(_stext % 4 == 0, " +ERROR(riscv-rt): \`_stext\` must be 4-byte aligned"); + +ASSERT(_sdata % 32 == 0 && _edata % 32 == 0, " +BUG(riscv-rt): .data is not 32-byte aligned"); + +ASSERT(_sidata % 32 == 0, " +BUG(riscv-rt): the LMA of .data is not 32-byte aligned"); + +ASSERT(_sbss % 32 == 0 && _ebss % 32 == 0, " +BUG(riscv-rt): .bss is not 32-byte aligned"); + +ASSERT(_sheap % 4 == 0, " +BUG(riscv-rt): start of .heap is not 4-byte aligned"); + +ASSERT(_stext + SIZEOF(.text) < ORIGIN(FLASH) + LENGTH(FLASH), " +ERROR(riscv-rt): The .text section must be placed inside the FLASH region. +Set _stext to an address smaller than 'ORIGIN(FLASH) + LENGTH(FLASH)'"); + +ASSERT(SIZEOF(.stack) > (_max_hart_id + 1) * _hart_stack_size, " +ERROR(riscv-rt): .stack section is too small for allocating stacks for all the harts. +Consider changing \`_max_hart_id\` or \`_hart_stack_size\`."); + +ASSERT(SIZEOF(.got) == 0, " +.got section detected in the input files. Dynamic relocations are not +supported. If you are linking to C code compiled using the \`gcc\` crate +then modify your build script to compile the C code _without_ the +-fPIC flag. See the documentation of the \`gcc::Config.fpic\` method for +details."); + +/* Do not exceed this mark in the error messages above | */ +`; + + try { + await writeFile(join(projectRoot, "rp2040.x"), rp2040X); + await writeFile(join(projectRoot, "rp2350.x"), rp2350X); + await writeFile(join(projectRoot, "rp2350_riscv.x"), rp2350RiscvX); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write memory.x files", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateBuildRs(projectRoot: string): Promise { + const buildRs = `//! Set up linker scripts for the rp235x-hal examples + +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +#[cfg(all(feature = "rp2040", feature = "rp2350"))] +compile_error!( + "\\"rp2040\\" and \\"rp2350\\" cannot be enabled at the same time - you must choose which to use" +); + +#[cfg(not(any(feature = "rp2040", feature = "rp2350")))] +compile_error!("You must enable either \\"rp2040\\" or \\"rp2350\\""); + +fn main() { + // 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()); + + // The file \`memory.x\` is loaded by cortex-m-rt's \`link.x\` script, which + // is what we specify in \`.cargo/config.toml\` for Arm builds + #[cfg(feature = "rp2040")] + let memory_x = include_bytes!("rp2040.x"); + #[cfg(feature = "rp2350")] + let memory_x = include_bytes!("rp2350.x"); + let mut f = File::create(out.join("memory.x")).unwrap(); + f.write_all(memory_x).unwrap(); + println!("cargo:rerun-if-changed=rp2040.x"); + println!("cargo:rerun-if-changed=rp2350.x"); + + // The file \`rp2350_riscv.x\` is what we specify in \`.cargo/config.toml\` for + // RISC-V builds + let rp2350_riscv_x = include_bytes!("rp2350_riscv.x"); + let mut f = File::create(out.join("rp2350_riscv.x")).unwrap(); + f.write_all(rp2350_riscv_x).unwrap(); + println!("cargo:rerun-if-changed=rp2350_riscv.x"); + + println!("cargo:rerun-if-changed=build.rs"); +}`; + + try { + await writeFile(join(projectRoot, "build.rs"), buildRs); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write build.rs file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateGitIgnore(projectRoot: string): Promise { + const gitIgnore = `# Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode,macos,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux +`; + + try { + await writeFile(join(projectRoot, ".gitignore"), gitIgnore); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write .gitignore file", + unknownErrorToString(error) + ); + + return false; + } +} + +/** + * Note: requires PICOTOOL_PATH to be set in the environment when running cargo. + * + * @param projectRoot The path where the project folder should be generated. + */ +async function generateCargoConfig(projectRoot: string): Promise { + const cargoConfig = `# +# Cargo Configuration for the https://github.com/rp-rs/rp-hal.git repository. +# +# You might want to make a similar file in your own repository if you are +# writing programs for Raspberry Silicon microcontrollers. +# + +[build] +# Set the default target to match the Cortex-M33 in the RP2350 +# target = "thumbv8m.main-none-eabihf" +target = "thumbv6m-none-eabi" +# target = "riscv32imac-unknown-none-elf" + +# Target specific options +[target.thumbv6m-none-eabi] +# Pass some extra options to rustc, some of which get passed on to the linker. +# +# * linker argument --nmagic turns off page alignment of sections (which saves +# flash space) +# * linker argument -Tlink.x tells the linker to use link.x as the linker +# script. This is usually provided by the cortex-m-rt crate, and by default +# the version in that crate will include a file called \`memory.x\` which +# describes the particular memory layout for your specific chip. +# * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't +# have SIMD) +rustflags = [ + "-C", "linker=flip-link", + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "no-vectorize-loops", +] + +# Use picotool for loading. +# +# Load an elf, skipping unchanged flash sectors, verify it, and execute it +runner = "\${PICOTOOL_PATH} load -u -v -x -t elf" + +# 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" + +# 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" +`; + + 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, + flashMethod: FlashMethod +): 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) { + Logger.error( + LoggerSource.projectRust, + "Failed to create project folder", + unknownErrorToString(error) + ); + + return false; + } + + // TODO: do all in parallel + let result = await generateCargoToml(projectFolder, projectName); + if (!result) { + return false; + } + + result = await generateMemoryLayouts(projectFolder); + if (!result) { + return false; + } + + result = await generateBuildRs(projectFolder); + if (!result) { + return false; + } + + result = await generateGitIgnore(projectFolder); + if (!result) { + return false; + } + + result = await generateCargoConfig(projectFolder); + if (!result) { + return false; + } + + result = await generateMainRs(projectFolder); + if (!result) { + return false; + } + + const picoSDK = await getSDKReleases(); + if (picoSDK.length === 0) { + Logger.error(LoggerSource.projectRust, "Failed to get SDK releases."); + + void window.showErrorMessage( + "Failed to get SDK releases. Please try again and check your settings." + ); + + return false; + } + result = await downloadAndInstallSDK(picoSDK[0], SDK_REPOSITORY_URL); + if (!result) { + Logger.error( + LoggerSource.projectRust, + "Failed to download and install SDK." + ); + + void window.showErrorMessage( + "Failed to download and install SDK. Please try again and check your settings." + ); + + return false; + } + + result = await generateVSCodeConfig(projectFolder, picoSDK[0]); + if (!result) { + return false; + } + + result = await initGit(projectFolder); + if (!result) { + return false; + } + + // add .pico-rs file + try { + await writeFile( + join(projectFolder, ".pico-rs"), + JSON.stringify({ flashMethod }) + ); + } 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..131a898e --- /dev/null +++ b/src/utils/projectGeneration/tomlUtil.mts @@ -0,0 +1,95 @@ +import { assert } from "console"; +import { writeFile } from "fs/promises"; + +// TODO: maybe not needed + +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." + ); + + 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" ? 1 : typeof value2 === "object" ? -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.length > 0 ? "\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 index 7dc90fcf..cb9c5a6e 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -8,12 +8,19 @@ import { env, ProgressLocation, Uri, window, workspace } from "vscode"; import { promisify } from "util"; import { exec } from "child_process"; import { dirname, join } from "path"; +import { parse as parseToml } from "toml"; /*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?: { @@ -539,63 +546,10 @@ export async function downloadAndInstallRust(): Promise { * @returns {boolean} True if all requirements are met or have been installed, false otherwise. */ export async function downloadAndInstallRust(): Promise { - /*try { - const rustup = process.platform === "win32" ? "rustup.exe" : "rustup"; - const cargo = process.platform === "win32" ? "cargo.exe" : "cargo"; - const commands = [ - `${rustup} target add thumbv6m-none-eabi`, - `${cargo} install flip-link`, - `${cargo} install --locked probe-rs-tools`, - `${cargo} install --locked elf2uf2-rs`, - ]; - - // merge all dirs up by one lvl - await mergeDirectories(targetDirectory); - - if (process.platform === "win32") { - // install portable MSVC - const result = await window.withProgress( - { - location: ProgressLocation.Notification, - title: "Installing MSVC", - cancellable: false, - }, - async () => installPortableMSVC() - ); - // TODO: error handling - if (!result) { - try { - rmSync(targetDirectory, { recursive: true, force: true }); - } catch { - /* */ - } - - return; - } - } - - // install dependencies - const cargoExecutable = joinPosix( - targetDirectory, - "bin", - "cargo" + (process.platform === "win32" ? ".exe" : "") - ); - - // TODO: add error handling - let result = await cargoInstall(cargoExecutable, "flip-link", false, {}); - if (!result) { - return undefined; - } - const hd = homedir().replaceAll("\\", "/"); - // TODO: install cmake - result = await cargoInstall( - cargoExecutable, - "probe-rs-tools", - true, - // TODO: load cmake version dynamically and download if not present - process.platform === "win32" - ? { - PATH: `${hd}/.pico-sdk/cmake/v3.28.6/bin;${hd}/.pico-sdk/rust/latest/bin;${hd}/.pico-sdk/msvc/latest/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64;${hd}/.pico-sdk/msvc/latest/Windows Kits/10/bin/10.0.19041.0/x64/ucrt`, + let result = await checkRustInstallation(); + if (!result) { + return false; + } // eslint-disable-next-line @typescript-eslint/naming-convention VSCMD_ARG_HOST_ARCH: "x64", @@ -639,7 +593,7 @@ export async function downloadAndInstallRust(): Promise { // install flip-link const flipLink = "flip-link"; - const result = await cargoInstall(flipLink, false); + result = await cargoInstall(flipLink, false); if (!result) { void window.showErrorMessage( `Failed to install cargo package '${flipLink}'.` + @@ -654,8 +608,8 @@ export async function downloadAndInstallRust(): Promise { // install probe-rs-tools const probeRsTools = "probe-rs-tools"; - const result2 = await cargoInstall(probeRsTools, true); - if (!result2) { + result = await cargoInstall(probeRsTools, true); + if (!result) { void window.showErrorMessage( `Failed to install cargo package '${probeRsTools}'.` + "Please check the logs." @@ -669,8 +623,8 @@ export async function downloadAndInstallRust(): Promise { // install elf2uf2-rs const elf2uf2Rs = "elf2uf2-rs"; - const result3 = await cargoInstall(elf2uf2Rs, true); - if (!result3) { + result = await cargoInstall(elf2uf2Rs, true); + if (!result) { void window.showErrorMessage( `Failed to install cargo package '${elf2uf2Rs}'.` + "Please check the logs." @@ -680,8 +634,8 @@ export async function downloadAndInstallRust(): Promise { } // install cargo-generate binary - const result4 = await installCargoGenerate(); - if (!result4) { + result = await installCargoGenerate(); + if (!result) { void window.showErrorMessage( "Failed to install cargo-generate. Please check the logs." ); @@ -780,10 +734,20 @@ async function installCargoGenerate(): Promise { 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: string + flashMethod: FlashMethod ): Promise { try { const valuesFile = join(tmpdir(), "pico-vscode", "values.toml"); @@ -791,13 +755,16 @@ export async function generateRustProject( await workspace.fs.writeFile( Uri.file(valuesFile), // TODO: make selectable in UI - Buffer.from(`[values]\nflash_method="${flashMethod}"\n`, "utf-8") + Buffer.from( + `[values]\nflash_method="${flashMethodToArg(flashMethod)}"\n`, + "utf-8" + ) ); // TODO: fix outside function (maybe) - let projectRoot = projectFolder; - if (projectFolder.endsWith(name)) { - projectRoot = projectFolder.slice(0, projectFolder.length - name.length); + let projectRoot = projectFolder.replaceAll("\\", "/"); + if (projectRoot.endsWith(name)) { + projectRoot = projectRoot.slice(0, projectRoot.length - name.length); } // cache template and use --path @@ -839,3 +806,39 @@ export async function generateRustProject( return true; } + +export async function chipFromCargoToml(): Promise { + const workspaceFolder = workspace.workspaceFolders?.[0]; + + if (!workspaceFolder) { + Logger.error(LoggerSource.rustUtil, "No workspace folder found."); + + return null; + } + + try { + const cargoTomlPath = join(workspaceFolder.uri.fsPath, "Cargo.toml"); + const contents = await workspace.fs.readFile(Uri.file(cargoTomlPath)); + + const cargoToml = (await parseToml(new TextDecoder().decode(contents))) as { + features?: { default?: string[] }; + }; + + const features = cargoToml.features?.default ?? []; + + if (features.includes("rp2040")) { + return "rp2040"; + } else if (features.includes("rp2350")) { + return "rp2350"; + } else if (features.includes("rp2350-riscv")) { + return "rp2350-RISCV"; + } + } catch (error) { + Logger.error( + LoggerSource.rustUtil, + `Failed to read Cargo.toml: ${unknownErrorToString(error)}` + ); + } + + return null; +} diff --git a/src/webview/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts index 16acd81a..b7728cfd 100644 --- a/src/webview/newRustProjectPanel.mts +++ b/src/webview/newRustProjectPanel.mts @@ -19,18 +19,18 @@ import { getProjectFolderDialogOptions, getWebviewOptions, } from "./newProjectPanel.mjs"; -import { existsSync, readFileSync } from "fs"; +import { existsSync } from "fs"; import { join } from "path"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; +import { downloadAndInstallRust } from "../utils/rustUtil.mjs"; import { - downloadAndInstallRust, + type FlashMethod, generateRustProject, -} from "../utils/rustUtil.mjs"; +} from "../utils/projectGeneration/projectRust.mjs"; interface SubmitMessageValue { projectName: string; - pythonMode: number; - pythonPath: string; + flashMethod: FlashMethod; } export class NewRustProjectPanel { @@ -234,7 +234,7 @@ export class NewRustProjectPanel { await window.withProgress( { location: ProgressLocation.Notification, - title: `Generating MicroPico project ${ + title: `Generating Rust Pico project ${ data.projectName ?? "undefined" } in ${this._projectRoot?.fsPath}...`, }, @@ -288,134 +288,19 @@ export class NewRustProjectPanel { return; } - // create the folder with project name in project root - - // create the project folder const projectFolder = join(projectPath, data.projectName); - progress.report({ - message: `Creating project folder ${projectFolder}`, - increment: 10, - }); - - try { - //await workspace.fs.createDirectory(Uri.file(projectFolder)); - // TODO: add flash method to ui - const result = await generateRustProject( - projectFolder, - data.projectName, - "probe-rs" - ); - if (!result) { - progress.report({ - message: "Failed", - increment: 100, - }); - void window.showErrorMessage( - `Failed to create project folder ${projectFolder}` - ); - - return; - } - - await workspace.fs.writeFile( - Uri.file(join(projectFolder, ".pico-rs")), - new Uint8Array() - ); - await workspace.fs.writeFile( - Uri.file(join(projectFolder, ".vscode", "extensions.json")), - Buffer.from( - JSON.stringify( - { - recommendations: [ - "rust-lang.rust-analyzer", - this._settings.getExtensionId(), - ], - }, - undefined, - 4 - ), - "utf-8" - ) - ); - await workspace.fs.writeFile( - Uri.file(join(projectFolder, ".vscode", "tasks.json")), - Buffer.from( - JSON.stringify( - { - version: "2.0.0", - tasks: [ - { - label: "Compile Project", - type: "process", - isBuildCommand: true, - command: "cargo", - args: ["build"], - group: { - kind: "build", - isDefault: true, - }, - presentation: { - reveal: "always", - panel: "dedicated", - }, - problemMatcher: "$rustc", - }, - { - label: "Run Project", - type: "shell", - command: "cargo", - args: ["run", "--release"], - group: { - kind: "test", - isDefault: true, - }, - problemMatcher: "$rustc", - }, - ], - }, - undefined, - 4 - ), - "utf-8" - ) - ); - const settingsFile = join(projectFolder, ".vscode", "settings.json"); - const settingsContent = existsSync(settingsFile) - ? (JSON.parse(readFileSync(settingsFile, "utf-8")) as - | { - [key: string]: unknown; - } - | undefined) - : {}; - if (!settingsContent) { - progress.report({ - message: "Failed", - increment: 100, - }); - void window.showErrorMessage( - `Failed to read settings file ${settingsFile}` - ); + const result = await generateRustProject( + projectFolder, + data.projectName, + data.flashMethod + ); - return; - } + if (!result) { + this._logger.error("Failed to generate Rust project."); - settingsContent["files.exclude"] = { - ...(settingsContent["files.exclude"] ?? {}), - // eslint-disable-next-line @typescript-eslint/naming-convention - ".pico-rs": true, - }; - await workspace.fs.writeFile( - Uri.file(settingsFile), - Buffer.from(JSON.stringify(settingsContent, null, 4), "utf-8") - ); - } catch { - progress.report({ - message: "Failed", - increment: 100, - }); - await window.showErrorMessage( - `Failed to create project folder ${projectFolder}` + void window.showErrorMessage( + "Failed to generate Rust project. Please try again and check your settings." ); return; @@ -658,6 +543,23 @@ export class NewRustProjectPanel { +
+

Flash Method

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

- Note: The Pico extension will always install and use the latest stable version of Rust. + Note: Ensure that you have the latest version of the Rustup toolchain manager installed.

From 0df00e271e8ce3c0a23adbdae9a6a92258a23be6 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:53:38 +0100 Subject: [PATCH 20/27] Add better error handling for failed flip-link install Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/utils/rustUtil.mts | 52 ++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index c43647ad..4014c411 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -82,8 +82,9 @@ function computeDownloadLink(release: string): string { export async function cargoInstall( packageName: string, locked = false -): Promise { +): Promise { const command = process.platform === "win32" ? "cargo.exe" : "cargo"; + try { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { stdout, stderr } = await execAsync( @@ -93,7 +94,7 @@ export async function cargoInstall( } ); - return true; + return; } catch (error) { const msg = unknownErrorToString(error); if ( @@ -109,7 +110,7 @@ export async function cargoInstall( msg ); - return true; + return; } Logger.error( LoggerSource.rustUtil, @@ -118,7 +119,7 @@ export async function cargoInstall( )}` ); - return false; + return unknownErrorToString(error); } } @@ -296,7 +297,7 @@ export async function checkRustInstallation(): Promise { * @returns {boolean} True if all requirements are met or have been installed, false otherwise. */ export async function downloadAndInstallRust(): Promise { - let result = await checkRustInstallation(); + const result = await checkRustInstallation(); if (!result) { return false; } @@ -329,20 +330,41 @@ export async function downloadAndInstallRust(): Promise { // install flip-link const flipLink = "flip-link"; - result = await cargoInstall(flipLink, false); - if (!result) { - void window.showErrorMessage( - `Failed to install cargo package '${flipLink}'.` + - "Please check the logs." - ); + 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"; - result = await cargoInstall(probeRsTools, true); - if (!result) { + cargoInstResult = await cargoInstall(probeRsTools, true); + if (!cargoInstResult) { void window.showErrorMessage( `Failed to install cargo package '${probeRsTools}'.` + "Please check the logs." @@ -353,8 +375,8 @@ export async function downloadAndInstallRust(): Promise { // install elf2uf2-rs const elf2uf2Rs = "elf2uf2-rs"; - result = await cargoInstall(elf2uf2Rs, true); - if (!result) { + cargoInstResult = await cargoInstall(elf2uf2Rs, true); + if (!cargoInstResult) { void window.showErrorMessage( `Failed to install cargo package '${elf2uf2Rs}'.` + "Please check the logs." From 6348d1022a952f958650b783df136be7e2913015 Mon Sep 17 00:00:00 2001 From: Paul <44974737+paulober@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:58:48 +0100 Subject: [PATCH 21/27] Add restart note to rustup install notification Co-authored-by: will-v-pi <108662275+will-v-pi@users.noreply.github.com> --- src/utils/rustUtil.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 4014c411..6b5b07be 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -262,7 +262,7 @@ export async function checkRustInstallation(): Promise { if (!rustupOk) { void window .showErrorMessage( - "Rustup is not installed. Please install it manually.", + "Rustup is not installed. Please install it manually and restart VS Code.", "Install" ) .then(result => { From 38adbdcb580b1a31b34148b3453def618982d77f Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:32:11 +0100 Subject: [PATCH 22/27] Fix cargo pkg installed error Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/utils/rustUtil.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 6b5b07be..16a1edce 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -364,7 +364,7 @@ export async function downloadAndInstallRust(): Promise { // or install probe-rs-tools const probeRsTools = "defmt-print"; cargoInstResult = await cargoInstall(probeRsTools, true); - if (!cargoInstResult) { + if (cargoInstResult !== undefined) { void window.showErrorMessage( `Failed to install cargo package '${probeRsTools}'.` + "Please check the logs." @@ -376,7 +376,7 @@ export async function downloadAndInstallRust(): Promise { // install elf2uf2-rs const elf2uf2Rs = "elf2uf2-rs"; cargoInstResult = await cargoInstall(elf2uf2Rs, true); - if (!cargoInstResult) { + if (cargoInstResult !== undefined) { void window.showErrorMessage( `Failed to install cargo package '${elf2uf2Rs}'.` + "Please check the logs." From 929a8d24c2e138d26b7bde130b8a5a48d32107c6 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:16:50 +0100 Subject: [PATCH 23/27] Fix python and sdk installation for new rust projects Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/utils/download.mts | 86 ++++++++++++++++++++++++++++++------ src/utils/rustUtil.mts | 3 +- src/utils/versionBundles.mts | 4 +- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/utils/download.mts b/src/utils/download.mts index f2cb7bb1..9258dec4 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -38,6 +38,7 @@ import { githubApiUnauthorized, HTTP_STATUS_FORBIDDEN, HTTP_STATUS_OK, + SDK_REPOSITORY_URL, } from "./githubREST.mjs"; import { unxzFile, unzipFile } from "./downloadHelpers.mjs"; import type { Writable } from "stream"; @@ -52,6 +53,7 @@ import { } 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 } = { @@ -574,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 || @@ -589,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; } @@ -1068,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 => { @@ -1327,16 +1334,66 @@ export async function installLatestRustRequirements( return false; } - const supportedToolchains = await getSupportedToolchains(); + // 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.toolchain + t => t.version === latest[1].toolchain ); if (toolchain === undefined) { @@ -1374,7 +1431,7 @@ export async function installLatestRustRequirements( }, async progress => { const toolchain = supportedToolchains.find( - t => t.version === latest.riscvToolchain + t => t.version === latest[1].riscvToolchain ); if (toolchain === undefined) { @@ -1437,11 +1494,14 @@ export async function installLatestRustRequirements( async progress => { let progressState = 0; - return downloadAndInstallPicotool(latest.picotool, (prog: Progress) => { - const percent = prog.percent * 100; - progress.report({ increment: percent - progressState }); - progressState = percent; - }); + return downloadAndInstallPicotool( + latest[1].picotool, + (prog: Progress) => { + const percent = prog.percent * 100; + progress.report({ increment: percent - progressState }); + progressState = percent; + } + ); } ); if (!result) { diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 16a1edce..72e25a47 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -262,7 +262,8 @@ export async function checkRustInstallation(): Promise { if (!rustupOk) { void window .showErrorMessage( - "Rustup is not installed. Please install it manually and restart VS Code.", + "Rustup is not installed. Please install it " + + "manually and restart VS Code.", "Install" ) .then(result => { diff --git a/src/utils/versionBundles.mts b/src/utils/versionBundles.mts index 56ef8cf2..b7ff3653 100644 --- a/src/utils/versionBundles.mts +++ b/src/utils/versionBundles.mts @@ -129,14 +129,14 @@ export default class VersionBundlesLoader { return chosenBundle; } - public async getLatest(): Promise { + 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][1]; + )[0]; } public async getLatestSDK(): Promise { From 8082a560cd75d78ce528ea12bddfb8528ee2c4b1 Mon Sep 17 00:00:00 2001 From: paulober Date: Mon, 1 Sep 2025 14:07:10 +0100 Subject: [PATCH 24/27] Add url to status code errors Signed-off-by: paulober --- src/utils/githubREST.mts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/githubREST.mts b/src/utils/githubREST.mts index 2bdfe45d..19ab639d 100644 --- a/src/utils/githubREST.mts +++ b/src/utils/githubREST.mts @@ -231,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 ); @@ -261,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) { From 9d75c80db61cd4cfbb93d355fc9d2cd0e53abcc2 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:29:21 +0100 Subject: [PATCH 25/27] Fix build issue Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/commands/getPaths.mts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/getPaths.mts b/src/commands/getPaths.mts index 0e79ba0d..c41cd91b 100644 --- a/src/commands/getPaths.mts +++ b/src/commands/getPaths.mts @@ -95,7 +95,7 @@ export class GetGDBPathCommand extends CommandWithResult { const supportedToolchains = await getSupportedToolchains(); const latestSupportedToolchain = supportedToolchains.find( - t => t.version === latestVb.toolchain + t => t.version === latestVb[1].toolchain ); if (!latestSupportedToolchain) { void window.showErrorMessage( @@ -110,8 +110,8 @@ export class GetGDBPathCommand extends CommandWithResult { )?.includes("riscv"); toolchainVersion = useRISCV - ? latestVb.riscvToolchain - : latestVb.toolchain; + ? latestVb[1].riscvToolchain + : latestVb[1].toolchain; } else { const selectedToolchainAndSDKVersions = await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri); From a2733c485d139f3a39e14f5044df6f4cbca81e31 Mon Sep 17 00:00:00 2001 From: paulober <44974737+paulober@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:50:35 +0100 Subject: [PATCH 26/27] Add better licensing for rust projects + name validation + remove debugger selection Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/commands/compileProject.mts | 11 +- src/utils/projectGeneration/projectRust.mts | 363 +++++++++++++++++++- src/utils/rustUtil.mts | 12 + src/webview/newRustProjectPanel.mts | 29 +- web/rust/main.js | 57 ++- 5 files changed, 398 insertions(+), 74 deletions(-) diff --git a/src/commands/compileProject.mts b/src/commands/compileProject.mts index 12096057..9940097e 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -16,15 +16,14 @@ 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 isRustProject = await commands.executeCommand( - "getContext", - ContextKeys.isRustProject - );*/ - const isRustProject = State.getInstance().isRustProject; const settings = Settings.getInstance(); if ( diff --git a/src/utils/projectGeneration/projectRust.mts b/src/utils/projectGeneration/projectRust.mts index d7e1f3ba..fe8f8b20 100644 --- a/src/utils/projectGeneration/projectRust.mts +++ b/src/utils/projectGeneration/projectRust.mts @@ -73,7 +73,7 @@ async function generateVSCodeConfig(projectRoot: string): Promise { ], }, ], - preLaunchTask: "Compile Project (debug)", + preLaunchTask: "Build + Generate SBOM (debug)", consoleLogLevel: "Debug", wireProtocol: "Swd", }, @@ -114,6 +114,31 @@ async function generateVSCodeConfig(projectRoot: string): Promise { }, }, }, + { + label: "Build + Generate SBOM (release)", + type: "shell", + command: "bash", + args: [ + "-lc", + `cargo sbom > \${command:${extensionName}.${LaunchTargetPathCommand.id}}/../../release/sbom.spdx.json`, + ], + windows: { + command: "powershell", + args: [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `cargo sbom | Set-Content -Encoding utf8 \${command:${extensionName}.${LaunchTargetPathCommand.id}}\\..\\..\\release\\sbom.spdx.json`, + ], + }, + dependsOn: "Compile Project", + presentation: { + reveal: "silent", + panel: "shared", + }, + problemMatcher: [], + }, { label: "Compile Project (debug)", type: "process", @@ -136,10 +161,35 @@ async function generateVSCodeConfig(projectRoot: string): Promise { }, }, }, + { + label: "Build + Generate SBOM (debug)", + type: "shell", + command: "bash", + args: [ + "-lc", + `cargo sbom --output-format spdx-json > \${command:${extensionName}.${LaunchTargetPathCommand.id}}/../../debug/sbom.spdx.json`, + ], + windows: { + command: "powershell", + args: [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `cargo sbom | Set-Content -Encoding utf8 \${command:${extensionName}.${LaunchTargetPathCommand.id}}\\..\\..\\debug\\sbom.spdx.json`, + ], + }, + dependsOn: "Compile Project (debug)", + presentation: { + reveal: "silent", + panel: "shared", + }, + problemMatcher: [], + }, { label: "Run Project", type: "shell", - dependsOn: "Compile Project", + dependsOn: ["Build + Generate SBOM (release)"], command: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, args: [ "load", @@ -180,13 +230,17 @@ async function generateVSCodeConfig(projectRoot: string): Promise { } async function generateMainRs(projectRoot: string): Promise { - const mainRs = `//! # GPIO 'Blinky' Example + 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. -//! -//! See the \`Cargo.toml\` file for Copyright and license details. #![no_std] #![no_main] @@ -319,6 +373,253 @@ pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 5] = [ } } +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, @@ -472,7 +773,15 @@ async function generateCargoToml( } async function generateMemoryLayouts(projectRoot: string): Promise { - const rp2040X = `MEMORY { + 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 @@ -557,7 +866,15 @@ SECTIONS { } INSERT AFTER .text; `; - const rp2350X = `MEMORY { + 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. * @@ -633,7 +950,15 @@ SECTIONS { PROVIDE(end_to_start = __start_block_addr - __end_block_addr); `; - const rp2350RiscvX = `MEMORY { + 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. * @@ -904,7 +1229,13 @@ details."); } async function generateBuildRs(projectRoot: string): Promise { - const buildRs = `//! Set up linker scripts for the rp235x-hal examples + 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; @@ -1120,7 +1451,12 @@ $RECYCLE.BIN/ * @param projectRoot The path where the project folder should be generated. */ async function generateCargoConfig(projectRoot: string): Promise { - const cargoConfig = `# + 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 @@ -1362,6 +1698,13 @@ export async function generateRustProject( 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( diff --git a/src/utils/rustUtil.mts b/src/utils/rustUtil.mts index 72e25a47..c575b246 100644 --- a/src/utils/rustUtil.mts +++ b/src/utils/rustUtil.mts @@ -386,6 +386,18 @@ export async function downloadAndInstallRust(): Promise { 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) { diff --git a/src/webview/newRustProjectPanel.mts b/src/webview/newRustProjectPanel.mts index 30db6dd5..3e585d7b 100644 --- a/src/webview/newRustProjectPanel.mts +++ b/src/webview/newRustProjectPanel.mts @@ -23,15 +23,11 @@ import { existsSync } from "fs"; import { join } from "path"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; import { downloadAndInstallRust } from "../utils/rustUtil.mjs"; -import { - type FlashMethod, - generateRustProject, -} from "../utils/projectGeneration/projectRust.mjs"; +import { generateRustProject } from "../utils/projectGeneration/projectRust.mjs"; import { installLatestRustRequirements } from "../utils/download.mjs"; interface SubmitMessageValue { projectName: string; - flashMethod: FlashMethod; } export class NewRustProjectPanel { @@ -297,11 +293,7 @@ export class NewRustProjectPanel { const projectFolder = join(projectPath, data.projectName); - result = await generateRustProject( - projectFolder, - data.projectName - //data.flashMethod - ); + result = await generateRustProject(projectFolder, data.projectName); if (!result) { this._logger.error("Failed to generate Rust project."); @@ -551,23 +543,6 @@ export class NewRustProjectPanel {
-
-

Flash Method

-
-
- - -
-
- - -
-
- - -
-
-