From fc24e8a8de18518a8133b3f917efc40e53df3241 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 1/9] Added experimental rust support Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- .vscodeignore | 1 + README.md | 6 + package.json | 14 +- scripts/portable-msvc.py | 372 ++++++++++++++ src/commands/compileProject.mts | 8 + src/commands/newProject.mts | 14 +- src/contextKeys.mts | 1 + src/extension.mts | 113 +++-- 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 +++++ yarn.lock | 8 + 19 files changed, 2083 insertions(+), 87 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 3e804bb4..329f08d4 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -31,6 +31,7 @@ tmp.py !scripts/lwipopts.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 542de47c..03c1d352 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ For optimal functionality, consider enabling: When prompted, select the `Pico` kit in CMake Tools, and set your build and launch targets accordingly. Use CMake Tools for compilation, but continue using this extension for debugging, as CMake Tools debugging is not compatible with Pico. +## Rust Prerequisites + +### 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: diff --git a/package.json b/package.json index c9311db9..9b4590e9 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", @@ -151,7 +152,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", @@ -162,7 +163,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", @@ -179,7 +180,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.importProject", @@ -200,7 +201,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" } ], "configuration": { @@ -307,6 +308,7 @@ "got": "^14.4.2", "ini": "^4.1.3", "rimraf": "^5.0.7", + "toml": "^3.0.0", "undici": "^6.19.7", "uuid": "^10.0.0", "which": "^4.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 7138472d..0f90a4e1 100644 --- a/src/commands/compileProject.mts +++ b/src/commands/compileProject.mts @@ -3,6 +3,8 @@ 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"; export default class CompileProjectCommand extends CommandWithResult { private _logger: Logger = new Logger("CompileProjectCommand"); @@ -18,9 +20,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 f37757de..8d995b71 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -76,6 +76,8 @@ 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"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -166,11 +168,15 @@ 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 is a pico project if ( workspaceFolder === undefined || - !existsSync(join(workspaceFolder.uri.fsPath, "pico_sdk_import.cmake")) + (!existsSync(join(workspaceFolder.uri.fsPath, "pico_sdk_import.cmake")) && + !isRustProject) ) { // finish activation Logger.warn( @@ -186,55 +192,80 @@ 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 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) - ) { - 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) + ) { + 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 4aa4970c..0ab8cad7 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -40,6 +40,7 @@ export enum LoggerSource { cmake = "cmakeUtil", downloadHelper = "downloadHelper", pythonHelper = "pythonHelper", + 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 6f647b89..867275e3 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 33f036ba..b5cc90af 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -35,6 +35,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"; @@ -178,6 +179,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. * @@ -203,7 +228,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 the SDK is already installed if ( @@ -273,7 +299,8 @@ export async function downloadAndInstallArchive( const unpackResult = await unpackArchive( archiveFilePath, targetDirectory, - archiveExtension + archiveExtension, + xzSingleDirOption ); if (unpackResult && extraCallback) { @@ -398,11 +425,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 bc86c6f7..f0e0ba0e 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 bf607649..76660839 100644 --- a/src/webview/activityBar.mts +++ b/src/webview/activityBar.mts @@ -41,6 +41,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"; @@ -86,6 +87,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"); @@ -179,6 +183,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) + }); +}()); diff --git a/yarn.lock b/yarn.lock index 1679c7e1..8a1f3c71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2468,6 +2468,7 @@ __metadata: ini: "npm:^4.1.3" rimraf: "npm:^5.0.7" rollup: "npm:^4.21.0" + toml: "npm:^3.0.0" tslib: "npm:^2.6.3" typescript: "npm:^5.5.4" typescript-eslint: "npm:^8.1.0" @@ -2909,6 +2910,13 @@ __metadata: languageName: node linkType: hard +"toml@npm:^3.0.0": + version: 3.0.0 + resolution: "toml@npm:3.0.0" + checksum: 10/cfef0966868d552bd02e741f30945a611f70841b7cddb07ea2b17441fe32543985bc0a7c0dcf7971af26fcaf8a17712a485d911f46bfe28644536e9a71a2bd09 + languageName: node + linkType: hard + "ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" From 9ed53e15267ae7557ba9ccc00e3ca421b864b8a7 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 2/9] 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 8d995b71..a866b655 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -254,7 +254,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 39de7a4ccc32cebfff3ee3b8ee4cf198addb76e0 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 3/9] Added rustup support Signed-off-by: paulober <44974737+paulober@users.noreply.github.com> --- src/commands/compileProject.mts | 1 - src/utils/download.mts | 19 ++- src/utils/githubREST.mts | 9 ++ src/utils/rustUtil.mts | 225 +++++++++++++++++++++++----- src/webview/newRustProjectPanel.mts | 167 ++++++--------------- 5 files changed, 263 insertions(+), 158 deletions(-) diff --git a/src/commands/compileProject.mts b/src/commands/compileProject.mts index 0f90a4e1..1f2e4e48 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"; export default class CompileProjectCommand extends CommandWithResult { diff --git a/src/utils/download.mts b/src/utils/download.mts index b5cc90af..46444b10 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -244,7 +244,7 @@ export async function downloadAndInstallArchive( // Ensure the target directory exists await mkdir(targetDirectory, { recursive: true }); - const archiveExtension = getArchiveExtension(url); + let archiveExtension = getArchiveExtension(url); if (!archiveExtension) { Logger.error( LoggerSource.downloader, @@ -254,6 +254,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); @@ -566,8 +579,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 f0e0ba0e..ab1fee69 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 f709a7c3..e9a244f8 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 { env, ProgressLocation, Uri, 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, execSync } from "child_process"; +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( packageName: string, @@ -97,6 +82,7 @@ export async function cargoInstall( ): Promise { const command = process.platform === "win32" ? "cargo.exe" : "cargo"; try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { stdout, stderr } = await execAsync( `${command} install ${locked ? "--locked " : ""}${packageName}`, { @@ -105,11 +91,14 @@ export async function cargoInstall( ); if (stderr) { + // TODO: find better solution if ( stderr.toLowerCase().includes("already exists") || - stderr.toLowerCase().includes("to your path") + stderr.toLowerCase().includes("to your path") || + stderr.toLowerCase().includes("is already installed") || + stderr.toLowerCase().includes("yanked in registry") ) { - Logger.warn( + Logger.debug( LoggerSource.rustUtil, `Cargo package '${packageName}' is already installed ` + "or cargo bin not in PATH:", @@ -132,7 +121,9 @@ export async function cargoInstall( const msg = unknownErrorToString(error); if ( msg.toLowerCase().includes("already exists") || - msg.toLowerCase().includes("to your path") + msg.toLowerCase().includes("to your path") || + msg.toLowerCase().includes("is already installed") || + msg.toLowerCase().includes("yanked in registry") ) { Logger.warn( LoggerSource.rustUtil, @@ -314,7 +305,13 @@ export async function checkRustInstallation(): Promise { } } -export async function installEmbeddedRust(): Promise { +/** + * 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"; @@ -378,8 +375,8 @@ export async function installEmbeddedRust(): Promise { const result = await cargoInstall(flipLink, false); if (!result) { void window.showErrorMessage( - `Failed to install cargo package '${flipLink}'.`, - "Please check the logs." + `Failed to install cargo package '${flipLink}'.` + + "Please check the logs." ); return false; @@ -390,8 +387,8 @@ export async function installEmbeddedRust(): Promise { const result2 = await cargoInstall(probeRsTools, true); if (!result2) { void window.showErrorMessage( - `Failed to install cargo package '${probeRsTools}'.`, - "Please check the logs." + `Failed to install cargo package '${probeRsTools}'.` + + "Please check the logs." ); return false; @@ -402,8 +399,106 @@ export async function installEmbeddedRust(): Promise { const result3 = await cargoInstall(elf2uf2Rs, true); if (!result3) { void window.showErrorMessage( - `Failed to install cargo package '${elf2uf2Rs}'.`, - "Please check the logs." + `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; @@ -412,4 +507,62 @@ export async function installEmbeddedRust(): Promise { return true; } -export +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 6b3f029a2f38a4d7ebe407174077d9c01f3e2675 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 4/9] 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 | 145 +- src/extension.mts | 35 +- src/logger.mts | 1 + src/ui.mts | 24 +- src/utils/projectGeneration/projectRust.mts | 1338 +++++++++++++++++++ src/utils/projectGeneration/tomlUtil.mts | 95 ++ src/utils/rustUtil.mts | 119 +- src/webview/newRustProjectPanel.mts | 162 +-- web/mpy/main.js | 1 - web/rust/main.js | 30 +- 14 files changed, 1890 insertions(+), 221 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 9b4590e9..a9a5c1d8 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", @@ -142,6 +148,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", @@ -152,7 +164,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", @@ -163,7 +175,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 622905d9..35696cf4 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" : ""}` ); } @@ -263,8 +267,10 @@ export class GetPicotoolPathCommand extends CommandWithResult< > { private running: boolean = false; + public static readonly id = "getPicotoolPath"; + constructor() { - super("getPicotoolPath"); + super(GetPicotoolPathCommand.id); } async execute(): Promise { @@ -305,3 +311,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 69951ba7..bb0cf376 100644 --- a/src/commands/launchTargetPath.mts +++ b/src/commands/launchTargetPath.mts @@ -3,10 +3,15 @@ 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"; export default class LaunchTargetPathCommand extends CommandWithResult { + public static readonly id = "launchTargetPath"; + constructor() { - super("launchTargetPath"); + super(LaunchTargetPathCommand.id); } private async readProjectNameFromCMakeLists( @@ -61,6 +66,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 && @@ -102,3 +134,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 717805d8..99fdfd4f 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, @@ -18,8 +23,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"; @@ -29,8 +40,9 @@ export default class SwitchBoardCommand extends Command { this._versionBundlesLoader = new VersionBundlesLoader(extensionUri); } - public static async askBoard(sdkVersion: string): - Promise<[string, boolean] | undefined> { + public static async askBoard( + sdkVersion: string + ): Promise<[string, boolean] | undefined> { const quickPickItems: string[] = ["pico", "pico_w"]; if (!compareLtMajor(sdkVersion, "2.0.0")) { @@ -51,17 +63,15 @@ export default class SwitchBoardCommand extends Command { }); if (board === undefined) { - return board; } // Check that board doesn't have an RP2040 on it const data = readFileSync( join(sdkPath, "src", "boards", "include", "boards", `${board}.h`) - ) + ); if (data.includes("rp2040")) { - return [board, false]; } @@ -70,7 +80,6 @@ export default class SwitchBoardCommand extends Command { }); if (useRiscV === undefined) { - return undefined; } @@ -79,15 +88,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 ); @@ -146,22 +256,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({ @@ -194,7 +301,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 a866b655..73076f95 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -31,7 +31,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, @@ -41,6 +43,7 @@ import { GetTargetCommand, GetChipUppercaseCommand, GetPicotoolPathCommand, + GetOpenOCDRootCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -76,8 +79,12 @@ 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 { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -104,6 +111,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(), @@ -112,6 +120,7 @@ export async function activate(context: ExtensionContext): Promise { new GetChipUppercaseCommand(), new GetTargetCommand(), new GetPicotoolPathCommand(), + new GetOpenOCDRootCommand(), new CompileProjectCommand(), new RunProjectCommand(), new FlashProjectSWDCommand(), @@ -160,6 +169,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, @@ -192,11 +212,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) { @@ -270,6 +290,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 0ab8cad7..2040d118 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -41,6 +41,7 @@ export enum LoggerSource { downloadHelper = "downloadHelper", pythonHelper = "pythonHelper", rustUtil = "rustUtil", + projectRust = "projectRust", } /** diff --git a/src/ui.mts b/src/ui.mts index 867275e3..c48002bd 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,9 +83,20 @@ export default class UI { } public updateBoard(board: string): void { - this._items[StatusBarItemKey.picoBoardQuickPick].text = STATUS_BAR_ITEMS[ - StatusBarItemKey.picoBoardQuickPick - ].text.replace("", 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); + } } /* 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 e9a244f8..45fcad1d 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?: { @@ -312,43 +319,10 @@ export async function checkRustInstallation(): 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`, - ]; - - return commands.every(command => { - try { - // TODO: test if throws on stderr - execSync(command, { - windowsHide: true, - }); - - return true; - } catch (error) { - Logger.error( - LoggerSource.rustUtil, - `Failed to execute command '${command}': ${unknownErrorToString( - error - )}` - ); - - return false; - } - }); - } catch (error) { - Logger.error( - LoggerSource.rustUtil, - `Failed to install embedded Rust: ${unknownErrorToString(error)}` - ); - + let result = await checkRustInstallation(); + if (!result) { return false; - }*/ + } // install target const target = "thumbv6m-none-eabi"; @@ -372,7 +346,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}'.` + @@ -384,8 +358,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." @@ -396,8 +370,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." @@ -407,8 +381,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." ); @@ -507,10 +481,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"); @@ -518,13 +502,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 @@ -566,3 +553,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

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