12 import { mkdir } from "fs/promises";
13 import { homedir, tmpdir } from "os";
14 import { basename, dirname, join, resolve } from "path";
15 import { join as joinPosix } from "path/posix";
16 import Logger from "../logger.mjs";
17 import { get } from "https";
18 import type { SupportedToolchainVersion } from "./toolchainUtil.mjs";
19 import { exec } from "child_process";
20 import { cloneRepository, initSubmodules } from "./gitUtil.mjs";
21 import { checkForInstallationRequirements } from "./requirementsUtil.mjs";
22 import { Octokit } from "octokit";
23 import { HOME_VAR, SettingsKey } from "../settings.mjs";
24 import type Settings from "../settings.mjs";
25 import AdmZip from "adm-zip";
26 import type { VersionBundle } from "./versionBundles.mjs";
27 import MacOSPythonPkgExtractor from "./macOSUtils.mjs";
28 import which from "which";
29 import { window } from "vscode";
30 import { fileURLToPath } from "url";
32 /// Translate nodejs platform names to ninja platform names
33 const NINJA_PLATFORMS: { [key: string]: string } = {
39 /// Translate nodejs platform names to cmake platform names
40 const CMAKE_PLATFORMS: { [key: string]: string } = {
46 export function buildToolchainPath(version: string): string {
47 // TODO: maybe put homedir() into a global
48 return joinPosix(homedir(), ".pico-sdk", "toolchain", version);
51 export function buildSDKPath(version: string): string {
52 // TODO: maybe replace . with _
54 homedir().replaceAll("\\", "/"),
61 export function buildToolsPath(version: string): string {
62 // TODO: maybe replace . with _
64 homedir().replaceAll("\\", "/"),
71 export function getScriptsRoot(): string {
73 dirname(fileURLToPath(import.meta.url)).replaceAll("\\", "/"),
79 export function buildNinjaPath(version: string): string {
81 homedir().replaceAll("\\", "/"),
88 export function buildCMakePath(version: string): string {
90 homedir().replaceAll("\\", "/"),
97 export function buildPython3Path(version: string): string {
99 homedir().replaceAll("\\", "/"),
106 function unzipFile(zipFilePath: string, targetDirectory: string): boolean {
108 const zip = new AdmZip(zipFilePath);
109 zip.extractAllTo(targetDirectory, true, true);
111 // TODO: improve this
112 const targetDirContents = readdirSync(targetDirectory);
113 const subfolderPath =
114 targetDirContents.length === 1
115 ? join(targetDirectory, targetDirContents[0])
118 process.platform === "win32" &&
119 targetDirContents.length === 1 &&
120 statSync(subfolderPath).isDirectory()
122 readdirSync(subfolderPath).forEach(item => {
123 const itemPath = join(subfolderPath, item);
124 const newItemPath = join(targetDirectory, item);
126 // Use fs.renameSync to move the item
127 renameSync(itemPath, newItemPath);
129 rmdirSync(subfolderPath);
135 `Error extracting archive file: ${
136 error instanceof Error ? error.message : (error as string)
145 * Extracts a .xz file using the 'tar' command.
147 * Also supports tar.gz files.
149 * Linux and macOS only.
152 * @param targetDirectory
155 async function unxzFile(
157 targetDirectory: string
158 ): Promise<boolean> {
159 if (process.platform === "win32") {
163 return new Promise<boolean>(resolve => {
165 // Construct the command to extract the .xz file using the 'tar' command
166 // -J option is redundant in modern versions of tar, but it's still good for compatibility
167 const command = `tar -x${
168 xzFilePath.endsWith(".xz") ? "J" : "z"
169 }f "${xzFilePath}" -C "${targetDirectory}"`;
171 // Execute the 'tar' command in the shell
172 exec(command, error => {
174 Logger.log(`Error extracting archive file: ${error?.message}`);
177 // Assuming there's only one subfolder in targetDirectory
178 const subfolder = readdirSync(targetDirectory)[0];
179 const subfolderPath = join(targetDirectory, subfolder);
181 // Move all files and folders from the subfolder to targetDirectory
182 readdirSync(subfolderPath).forEach(item => {
183 const itemPath = join(subfolderPath, item);
184 const newItemPath = join(targetDirectory, item);
186 // Use fs.renameSync to move the item
187 renameSync(itemPath, newItemPath);
190 // Remove the empty subfolder
191 rmdirSync(subfolderPath);
193 Logger.log(`Extracted archive file: ${xzFilePath}`);
203 export async function downloadAndInstallSDK(
205 repositoryUrl: string,
208 ): Promise<boolean> {
209 let gitExecutable: string | undefined =
211 .getString(SettingsKey.gitPath)
212 ?.replace(HOME_VAR, homedir().replaceAll("\\", "/")) || "git";
214 // TODO: this does take about 2s - may be reduced
215 const requirementsCheck = await checkForInstallationRequirements(
219 if (!requirementsCheck) {
223 const targetDirectory = buildSDKPath(version);
225 // Check if the SDK is already installed
226 if (existsSync(targetDirectory)) {
227 Logger.log(`SDK ${version} is already installed.`);
232 // Ensure the target directory exists
233 //await mkdir(targetDirectory, { recursive: true });
234 const gitPath = await which(gitExecutable, { nothrow: true });
235 if (gitPath === null) {
236 // if git is not in path then checkForInstallationRequirements
237 // maye downloaded it, so reload
239 gitExecutable = settings
240 .getString(SettingsKey.gitPath)
241 ?.replace(HOME_VAR, homedir().replaceAll("\\", "/"));
242 if (gitExecutable === null) {
243 Logger.log("Error: Git not found.");
245 await window.showErrorMessage(
246 "Git not found. Please install and add to PATH or " +
247 "set the path to the git executable in global settings."
253 // using deferred execution to avoid git clone if git is not available
256 (await cloneRepository(
264 // check python requirements
265 const python3Exe: string =
268 .getString(SettingsKey.python3Path)
269 ?.replace(HOME_VAR, homedir().replaceAll("\\", "/")) ||
270 (process.platform === "win32" ? "python" : "python3");
271 const python3: string | null = await which(python3Exe, { nothrow: true });
273 if (python3 === null) {
275 "Error: Python3 is not installed and could not be downloaded."
278 void window.showErrorMessage("Python3 is not installed and in PATH.");
283 return initSubmodules(targetDirectory, gitExecutable);
289 export function downloadAndInstallTools(
292 const targetDirectory = buildToolsPath(version);
294 // Check if the SDK is already installed
295 if (existsSync(targetDirectory)) {
296 Logger.log(`SDK Tools ${version} is already installed.`);
301 // Check we are on a supported OS
302 if (process.platform !== "win32" ||
303 (process.platform === "win32" && process.arch !== "x64")) {
304 Logger.log("Not installing SDK Tools as not on windows");
309 Logger.log(`Installing SDK Tools ${version}`);
311 // Ensure the target directory exists
312 // await mkdir(targetDirectory, { recursive: true });
314 cp(joinPosix(getScriptsRoot(), `tools/${version}`),
316 { recursive: true }, function(err) {
317 Logger.log(err?.message || "No error");
318 Logger.log(err?.code || "No code");
326 export async function downloadAndInstallToolchain(
327 toolchain: SupportedToolchainVersion,
329 ): Promise<boolean> {
330 const targetDirectory = buildToolchainPath(toolchain.version);
332 // Check if the SDK is already installed
333 if (redirectURL === undefined && existsSync(targetDirectory)) {
334 Logger.log(`Toolchain ${toolchain.version} is already installed.`);
339 // Ensure the target directory exists
340 await mkdir(targetDirectory, { recursive: true });
342 // select download url for platform()_arch()
343 const platformDouble = `${process.platform}_${process.arch}`;
344 const downloadUrl = redirectURL ?? toolchain.downloadUrls[platformDouble];
345 const basenameSplit = basename(downloadUrl).split(".");
346 let artifactExt = basenameSplit.pop();
347 if (artifactExt === "xz" || artifactExt === "gz") {
348 artifactExt = basenameSplit.pop() + "." + artifactExt;
351 if (artifactExt === undefined) {
355 const tmpBasePath = join(tmpdir(), "pico-sdk");
356 await mkdir(tmpBasePath, { recursive: true });
357 const archiveFilePath = join(
359 `${toolchain.version}.${artifactExt}`
362 return new Promise(resolve => {
363 const requestOptions = {
365 // eslint-disable-next-line @typescript-eslint/naming-convention
366 "User-Agent": "VSCode-RaspberryPi-Pico-Extension",
367 // eslint-disable-next-line @typescript-eslint/naming-convention
369 // eslint-disable-next-line @typescript-eslint/naming-convention
370 "Accept-Encoding": "gzip, deflate, br",
374 get(downloadUrl, requestOptions, response => {
375 const code = response.statusCode ?? 404;
378 //return reject(new Error(response.statusMessage));
380 "Error while downloading toolchain: " + response.statusMessage
383 return resolve(false);
387 if (code > 300 && code < 400 && !!response.headers.location) {
389 downloadAndInstallToolchain(toolchain, response.headers.location)
393 // save the file to disk
394 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
395 // unpack the archive
396 if (artifactExt === "tar.xz" || artifactExt === "tar.gz") {
397 unxzFile(archiveFilePath, targetDirectory)
400 unlinkSync(archiveFilePath);
404 unlinkSync(archiveFilePath);
405 unlinkSync(targetDirectory);
408 } else if (artifactExt === "zip") {
409 const success = unzipFile(archiveFilePath, targetDirectory);
411 unlinkSync(archiveFilePath);
413 unlinkSync(targetDirectory);
417 unlinkSync(archiveFilePath);
418 unlinkSync(targetDirectory);
419 Logger.log(`Error: unknown archive extension: ${artifactExt}`);
424 response.pipe(fileWriter);
425 }).on("error", () => {
427 unlinkSync(archiveFilePath);
428 unlinkSync(targetDirectory);
429 Logger.log("Error while downloading toolchain.");
436 export async function downloadAndInstallNinja(
439 ): Promise<boolean> {
440 /*if (process.platform === "linux") {
441 Logger.log("Ninja installation on Linux is not supported.");
446 const targetDirectory = buildNinjaPath(version);
448 // Check if the SDK is already installed
449 if (redirectURL === undefined && existsSync(targetDirectory)) {
450 Logger.log(`Ninja ${version} is already installed.`);
455 // Ensure the target directory exists
456 await mkdir(targetDirectory, { recursive: true });
458 const tmpBasePath = join(tmpdir(), "pico-sdk");
459 await mkdir(tmpBasePath, { recursive: true });
460 const archiveFilePath = join(tmpBasePath, `ninja.zip`);
462 const octokit = new Octokit();
463 // eslint-disable-next-line @typescript-eslint/naming-convention
464 let ninjaAsset: { name: string; browser_download_url: string } | undefined;
467 if (redirectURL === undefined) {
468 const releaseResponse = await octokit.rest.repos.getReleaseByTag({
469 owner: "ninja-build",
474 releaseResponse.status !== 200 &&
475 releaseResponse.data === undefined
477 Logger.log(`Error fetching ninja release ${version}.`);
481 const release = releaseResponse.data;
482 const assetName = `ninja-${NINJA_PLATFORMS[process.platform]}.zip`;
484 // Find the asset with the name 'ninja-win.zip'
485 ninjaAsset = release.assets.find(asset => asset.name === assetName);
489 // eslint-disable-next-line @typescript-eslint/naming-convention
490 browser_download_url: redirectURL,
495 `Error fetching ninja release ${version}. ${
496 error instanceof Error ? error.message : (error as string)
504 Logger.log(`Error release asset for ninja release ${version} not found.`);
509 // Download the asset
510 const assetUrl = ninjaAsset.browser_download_url;
512 return new Promise(resolve => {
513 // Use https.get to download the asset
514 get(assetUrl, response => {
515 const code = response.statusCode ?? 404;
517 // redirects not supported
519 //return reject(new Error(response.statusMessage));
520 Logger.log("Error while downloading ninja: " + response.statusMessage);
522 return resolve(false);
526 if (code > 300 && code < 400 && !!response.headers.location) {
528 downloadAndInstallNinja(version, response.headers.location)
532 // save the file to disk
533 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
534 // unpack the archive
535 const success = unzipFile(archiveFilePath, targetDirectory);
538 unlinkSync(archiveFilePath);
540 // unzipper would require custom permission handling as it
541 // doesn't preserve the executable flag
542 /*if (process.platform !== "win32") {
543 chmodSync(join(targetDirectory, "ninja"), 0o755);
549 response.pipe(fileWriter);
550 }).on("error", error => {
551 Logger.log("Error downloading asset:" + error.message);
558 * Supports Windows and macOS amd64 and arm64.
563 export async function downloadAndInstallCmake(
566 ): Promise<boolean> {
567 /*if (process.platform === "linux") {
568 Logger.log("CMake installation on Linux is not supported.");
573 const targetDirectory = buildCMakePath(version);
575 // Check if the SDK is already installed
576 if (redirectURL === undefined && existsSync(targetDirectory)) {
577 Logger.log(`CMake ${version} is already installed.`);
582 // Ensure the target directory exists
583 await mkdir(targetDirectory, { recursive: true });
584 const assetExt = process.platform === "win32" ? "zip" : "tar.gz";
586 const tmpBasePath = join(tmpdir(), "pico-sdk");
587 await mkdir(tmpBasePath, { recursive: true });
588 const archiveFilePath = join(tmpBasePath, `cmake-${version}.${assetExt}`);
590 const octokit = new Octokit();
592 // eslint-disable-next-line @typescript-eslint/naming-convention
593 let cmakeAsset: { name: string; browser_download_url: string } | undefined;
596 if (redirectURL === undefined) {
597 const releaseResponse = await octokit.rest.repos.getReleaseByTag({
603 releaseResponse.status !== 200 &&
604 releaseResponse.data === undefined
606 Logger.log(`Error fetching CMake release ${version}.`);
610 const release = releaseResponse.data;
611 const assetName = `cmake-${version.replace("v", "")}-${
612 CMAKE_PLATFORMS[process.platform]
614 process.platform === "darwin"
616 : process.arch === "arm64"
617 ? process.platform === "linux"
623 cmakeAsset = release.assets.find(asset => asset.name === assetName);
627 // eslint-disable-next-line @typescript-eslint/naming-convention
628 browser_download_url: redirectURL,
633 `Error fetching CMake release ${version}. ${
634 error instanceof Error ? error.message : (error as string)
642 Logger.log(`Error release asset for cmake release ${version} not found.`);
647 // Download the asset
648 const assetUrl = cmakeAsset.browser_download_url;
650 return new Promise(resolve => {
651 // Use https.get to download the asset
652 get(assetUrl, response => {
653 const code = response.statusCode ?? 0;
655 // redirects not supported
657 //return reject(new Error(response.statusMessage));
659 "Error while downloading toolchain: " + response.statusMessage
662 return resolve(false);
666 if (code > 300 && code < 400 && !!response.headers.location) {
668 downloadAndInstallCmake(version, response.headers.location)
672 // save the file to disk
673 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
674 // unpack the archive
675 if (process.platform === "darwin" || process.platform === "linux") {
676 unxzFile(archiveFilePath, targetDirectory)
679 unlinkSync(archiveFilePath);
681 //chmodSync(join(targetDirectory, "CMake.app", "Contents", "bin", "cmake"), 0o755);
685 unlinkSync(archiveFilePath);
686 unlinkSync(targetDirectory);
689 } else if (process.platform === "win32") {
690 const success = unzipFile(archiveFilePath, targetDirectory);
692 unlinkSync(archiveFilePath);
695 Logger.log(`Error: platform not supported for downloading cmake.`);
696 unlinkSync(archiveFilePath);
697 unlinkSync(targetDirectory);
703 response.pipe(fileWriter);
704 }).on("error", error => {
705 Logger.log("Error downloading asset: " + error.message);
712 * Only supported Windows amd64 and arm64.
716 export async function downloadEmbedPython(
717 versionBundle: VersionBundle,
719 ): Promise<string | undefined> {
721 // even tough this function supports downloading python3 on macOS arm64
722 // it doesn't work correctly therefore it's excluded here
723 // use pyenvInstallPython instead
724 process.platform !== "win32" ||
725 (process.platform === "win32" && process.arch !== "x64")
728 "Embed Python installation on Windows x64 and macOS arm64 only."
734 const targetDirectory = buildPython3Path(versionBundle.python.version);
735 const settingsTargetDirectory =
736 `${HOME_VAR}/.pico-sdk` + `/python/${versionBundle.python.version}`;
738 // Check if the Embed Python is already installed
739 if (redirectURL === undefined && existsSync(targetDirectory)) {
740 Logger.log(`Embed Python is already installed correctly.`);
742 return `${settingsTargetDirectory}/python.exe`;
745 // Ensure the target directory exists
746 await mkdir(targetDirectory, { recursive: true });
748 // select download url
749 const downloadUrl = versionBundle.python.windowsAmd64;
751 const tmpBasePath = join(tmpdir(), "pico-sdk");
752 await mkdir(tmpBasePath, { recursive: true });
753 const archiveFilePath = join(
755 `python-${versionBundle.python.version}.zip`
758 return new Promise(resolve => {
759 const requestOptions = {
761 // eslint-disable-next-line @typescript-eslint/naming-convention
762 "User-Agent": "VSCode-RaspberryPi-Pico-Extension",
763 // eslint-disable-next-line @typescript-eslint/naming-convention
765 // eslint-disable-next-line @typescript-eslint/naming-convention
766 "Accept-Encoding": "gzip, deflate, br",
770 get(downloadUrl, requestOptions, response => {
771 const code = response.statusCode ?? 0;
774 //return reject(new Error(response.statusMessage));
775 Logger.log("Error while downloading python: " + response.statusMessage);
777 return resolve(undefined);
781 if (code > 300 && code < 400 && !!response.headers.location) {
783 downloadEmbedPython(versionBundle, response.headers.location)
787 // save the file to disk
788 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
789 // doesn't work correctly therefore use pyenvInstallPython instead
790 // TODO: remove unused darwin code-path here
791 if (process.platform === "darwin") {
792 const pkgExtractor = new MacOSPythonPkgExtractor(
800 if (versionBundle.python.version.lastIndexOf(".") <= 2) {
802 "Error while extracting Python: " +
803 "Python version has wrong format."
810 // create symlink, so the same path can be used as on Windows
811 const srcPath = joinPosix(
812 settingsTargetDirectory,
814 versionBundle.python.version.substring(
816 versionBundle.python.version.lastIndexOf(".")
823 // use .exe as python is already used in the directory
824 join(settingsTargetDirectory, "python.exe"),
829 // use .exe as python is already used in the directory
830 join(settingsTargetDirectory, "python3.exe"),
837 resolve(`${settingsTargetDirectory}/python.exe`);
846 // unpack the archive
847 const success = unzipFile(archiveFilePath, targetDirectory);
849 unlinkSync(archiveFilePath);
851 success ? `${settingsTargetDirectory}/python.exe` : undefined
856 response.pipe(fileWriter);
857 }).on("error", () => {
858 Logger.log("Error while downloading Embed Python.");
865 const GIT_DOWNLOAD_URL_WIN_AMD64 =
866 "https://github.com/git-for-windows/git/releases/download" +
867 "/v2.43.0.windows.1/MinGit-2.43.0-64-bit.zip";
868 const GIT_MACOS_VERSION = "2.43.0";
869 const GIT_DOWNLOAD_URL_MACOS_ARM64 =
870 "https://bd752571.vscode-raspberry-pi-pico.pages.dev" +
871 "/git-2.43.0-arm64_sonoma.bottle.tar.gz";
872 const GIT_DOWNLOAD_URL_MACOS_INTEL =
873 "https://bd752571.vscode-raspberry-pi-pico.pages.dev" +
874 "/git-2.43.0-intel_sonoma.bottle.tar.gz";
877 * Only supported Windows amd64 and macOS arm64 and amd64.
881 export async function downloadGit(
883 ): Promise<string | undefined> {
885 process.platform !== "win32" ||
886 (process.platform === "win32" && process.arch !== "x64")
888 Logger.log("Git installation on Windows x64 and macOS only.");
893 const targetDirectory = join(homedir(), ".pico-sdk", "git");
894 const settingsTargetDirectory = `${HOME_VAR}/.pico-sdk/git`;
896 // Check if the Embed Python is already installed
897 if (redirectURL === undefined && existsSync(targetDirectory)) {
898 Logger.log(`Git is already installed.`);
900 return process.platform === "win32"
901 ? `${settingsTargetDirectory}/cmd/git.exe`
902 : `${settingsTargetDirectory}/bin/git`;
905 // Ensure the target directory exists
906 await mkdir(targetDirectory, { recursive: true });
908 // select download url for platform()_arch()
909 const downloadUrl = redirectURL ?? GIT_DOWNLOAD_URL_WIN_AMD64;
911 const tmpBasePath = join(tmpdir(), "pico-sdk");
912 await mkdir(tmpBasePath, { recursive: true });
913 const archiveFilePath = join(tmpBasePath, `git.zip`);
915 return new Promise(resolve => {
916 const requestOptions = {
918 // eslint-disable-next-line @typescript-eslint/naming-convention
919 "User-Agent": "VSCode-RaspberryPi-Pico-Extension",
920 // eslint-disable-next-line @typescript-eslint/naming-convention
922 // eslint-disable-next-line @typescript-eslint/naming-convention
923 "Accept-Encoding": "gzip, deflate, br",
927 get(downloadUrl, requestOptions, response => {
928 const code = response.statusCode ?? 0;
931 //return reject(new Error(response.statusMessage));
932 Logger.log("Error while downloading git: " + response.statusMessage);
938 if (code > 300 && code < 400 && !!response.headers.location) {
939 return resolve(downloadGit(response.headers.location));
942 // save the file to disk
943 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
944 // TODO: remove unused code-path here
945 if (process.platform === "darwin") {
946 unxzFile(archiveFilePath, targetDirectory)
948 unlinkSync(archiveFilePath);
950 success ? `${settingsTargetDirectory}/bin/git` : undefined
957 // unpack the archive
958 const success = unzipFile(archiveFilePath, targetDirectory);
960 unlinkSync(archiveFilePath);
963 // remove include section from gitconfig included in MiniGit
964 // which hardcodes the a path in Programm Files to be used by this git executable
967 process.env.ComSpec === "powershell.exe" ? "&" : ""
968 }"${targetDirectory}/cmd/git.exe" config ` +
969 `--file "${targetDirectory}/etc/gitconfig" ` +
970 "--remove-section include",
974 `Error executing git: ${
975 error instanceof Error ? error.message : (error as string)
980 resolve(`${settingsTargetDirectory}/cmd/git.exe`);
990 response.pipe(fileWriter);
991 }).on("error", () => {
992 Logger.log("Error while downloading git.");