]> Git Repo - pico-vscode.git/blob - src/utils/download.mts
Add elf2uf2 and pioasm for sdk 1.5.1
[pico-vscode.git] / src / utils / download.mts
1 import {
2   cp,
3   createWriteStream,
4   existsSync,
5   readdirSync,
6   renameSync,
7   rmdirSync,
8   statSync,
9   symlinkSync,
10   unlinkSync,
11 } from "fs";
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";
31
32 /// Translate nodejs platform names to ninja platform names
33 const NINJA_PLATFORMS: { [key: string]: string } = {
34   darwin: "mac",
35   linux: "lin",
36   win32: "win",
37 };
38
39 /// Translate nodejs platform names to cmake platform names
40 const CMAKE_PLATFORMS: { [key: string]: string } = {
41   darwin: "macos",
42   linux: "linux",
43   win32: "windows",
44 };
45
46 export function buildToolchainPath(version: string): string {
47   // TODO: maybe put homedir() into a global
48   return joinPosix(homedir(), ".pico-sdk", "toolchain", version);
49 }
50
51 export function buildSDKPath(version: string): string {
52   // TODO: maybe replace . with _
53   return joinPosix(
54     homedir().replaceAll("\\", "/"),
55     ".pico-sdk",
56     "sdk",
57     version
58   );
59 }
60
61 export function buildToolsPath(version: string): string {
62   // TODO: maybe replace . with _
63   return joinPosix(
64     homedir().replaceAll("\\", "/"),
65     ".pico-sdk",
66     "tools",
67     version
68   );
69 }
70
71 export function getScriptsRoot(): string {
72   return joinPosix(
73     dirname(fileURLToPath(import.meta.url)).replaceAll("\\", "/"),
74     "..",
75     "scripts"
76   );
77 }
78
79 export function buildNinjaPath(version: string): string {
80   return joinPosix(
81     homedir().replaceAll("\\", "/"),
82     ".pico-sdk",
83     "ninja",
84     version
85   );
86 }
87
88 export function buildCMakePath(version: string): string {
89   return joinPosix(
90     homedir().replaceAll("\\", "/"),
91     ".pico-sdk",
92     "cmake",
93     version
94   );
95 }
96
97 export function buildPython3Path(version: string): string {
98   return joinPosix(
99     homedir().replaceAll("\\", "/"),
100     ".pico-sdk",
101     "python",
102     version
103   );
104 }
105
106 function unzipFile(zipFilePath: string, targetDirectory: string): boolean {
107   try {
108     const zip = new AdmZip(zipFilePath);
109     zip.extractAllTo(targetDirectory, true, true);
110
111     // TODO: improve this
112     const targetDirContents = readdirSync(targetDirectory);
113     const subfolderPath =
114       targetDirContents.length === 1
115         ? join(targetDirectory, targetDirContents[0])
116         : "";
117     if (
118       process.platform === "win32" &&
119       targetDirContents.length === 1 &&
120       statSync(subfolderPath).isDirectory()
121     ) {
122       readdirSync(subfolderPath).forEach(item => {
123         const itemPath = join(subfolderPath, item);
124         const newItemPath = join(targetDirectory, item);
125
126         // Use fs.renameSync to move the item
127         renameSync(itemPath, newItemPath);
128       });
129       rmdirSync(subfolderPath);
130     }
131
132     return true;
133   } catch (error) {
134     Logger.log(
135       `Error extracting archive file: ${
136         error instanceof Error ? error.message : (error as string)
137       }`
138     );
139
140     return false;
141   }
142 }
143
144 /**
145  * Extracts a .xz file using the 'tar' command.
146  *
147  * Also supports tar.gz files.
148  *
149  * Linux and macOS only.
150  *
151  * @param xzFilePath
152  * @param targetDirectory
153  * @returns
154  */
155 async function unxzFile(
156   xzFilePath: string,
157   targetDirectory: string
158 ): Promise<boolean> {
159   if (process.platform === "win32") {
160     return false;
161   }
162
163   return new Promise<boolean>(resolve => {
164     try {
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}"`;
170
171       // Execute the 'tar' command in the shell
172       exec(command, error => {
173         if (error) {
174           Logger.log(`Error extracting archive file: ${error?.message}`);
175           resolve(false);
176         } else {
177           // Assuming there's only one subfolder in targetDirectory
178           const subfolder = readdirSync(targetDirectory)[0];
179           const subfolderPath = join(targetDirectory, subfolder);
180
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);
185
186             // Use fs.renameSync to move the item
187             renameSync(itemPath, newItemPath);
188           });
189
190           // Remove the empty subfolder
191           rmdirSync(subfolderPath);
192
193           Logger.log(`Extracted archive file: ${xzFilePath}`);
194           resolve(true);
195         }
196       });
197     } catch (error) {
198       resolve(false);
199     }
200   });
201 }
202
203 export async function downloadAndInstallSDK(
204   version: string,
205   repositoryUrl: string,
206   settings: Settings,
207   python3Path?: string
208 ): Promise<boolean> {
209   let gitExecutable: string | undefined =
210     settings
211       .getString(SettingsKey.gitPath)
212       ?.replace(HOME_VAR, homedir().replaceAll("\\", "/")) || "git";
213
214   // TODO: this does take about 2s - may be reduced
215   const requirementsCheck = await checkForInstallationRequirements(
216     settings,
217     gitExecutable
218   );
219   if (!requirementsCheck) {
220     return false;
221   }
222
223   const targetDirectory = buildSDKPath(version);
224
225   // Check if the SDK is already installed
226   if (existsSync(targetDirectory)) {
227     Logger.log(`SDK ${version} is already installed.`);
228
229     return true;
230   }
231
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
238     settings.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.");
244
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."
248       );
249
250       return false;
251     }
252   }
253   // using deferred execution to avoid git clone if git is not available
254   if (
255     gitPath !== null &&
256     (await cloneRepository(
257       repositoryUrl,
258       version,
259       targetDirectory,
260       gitExecutable
261     ))
262   ) {
263     settings.reload();
264     // check python requirements
265     const python3Exe: string =
266       python3Path ||
267       settings
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 });
272
273     if (python3 === null) {
274       Logger.log(
275         "Error: Python3 is not installed and could not be downloaded."
276       );
277
278       void window.showErrorMessage("Python3 is not installed and in PATH.");
279
280       return false;
281     }
282
283     return initSubmodules(targetDirectory, gitExecutable);
284   }
285
286   return false;
287 }
288
289 export function downloadAndInstallTools(
290   version: string,
291 ): boolean {
292   const targetDirectory = buildToolsPath(version);
293
294   // Check if the SDK is already installed
295   if (existsSync(targetDirectory)) {
296     Logger.log(`SDK Tools ${version} is already installed.`);
297
298     return true;
299   }
300
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");
305
306     return true;
307   }
308
309   Logger.log(`Installing SDK Tools ${version}`);
310
311   // Ensure the target directory exists
312   // await mkdir(targetDirectory, { recursive: true });
313
314   cp(joinPosix(getScriptsRoot(), `tools/${version}`),
315     targetDirectory,
316     { recursive: true }, function(err) {
317       Logger.log(err?.message || "No error");
318       Logger.log(err?.code || "No code");
319       resolve();
320     }
321   );
322
323   return true;
324 }
325
326 export async function downloadAndInstallToolchain(
327   toolchain: SupportedToolchainVersion,
328   redirectURL?: string
329 ): Promise<boolean> {
330   const targetDirectory = buildToolchainPath(toolchain.version);
331
332   // Check if the SDK is already installed
333   if (redirectURL === undefined && existsSync(targetDirectory)) {
334     Logger.log(`Toolchain ${toolchain.version} is already installed.`);
335
336     return true;
337   }
338
339   // Ensure the target directory exists
340   await mkdir(targetDirectory, { recursive: true });
341
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;
349   }
350
351   if (artifactExt === undefined) {
352     return false;
353   }
354
355   const tmpBasePath = join(tmpdir(), "pico-sdk");
356   await mkdir(tmpBasePath, { recursive: true });
357   const archiveFilePath = join(
358     tmpBasePath,
359     `${toolchain.version}.${artifactExt}`
360   );
361
362   return new Promise(resolve => {
363     const requestOptions = {
364       headers: {
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
368         Accept: "*/*",
369         // eslint-disable-next-line @typescript-eslint/naming-convention
370         "Accept-Encoding": "gzip, deflate, br",
371       },
372     };
373
374     get(downloadUrl, requestOptions, response => {
375       const code = response.statusCode ?? 404;
376
377       if (code >= 400) {
378         //return reject(new Error(response.statusMessage));
379         Logger.log(
380           "Error while downloading toolchain: " + response.statusMessage
381         );
382
383         return resolve(false);
384       }
385
386       // handle redirects
387       if (code > 300 && code < 400 && !!response.headers.location) {
388         return resolve(
389           downloadAndInstallToolchain(toolchain, response.headers.location)
390         );
391       }
392
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)
398             .then(success => {
399               // delete tmp file
400               unlinkSync(archiveFilePath);
401               resolve(success);
402             })
403             .catch(() => {
404               unlinkSync(archiveFilePath);
405               unlinkSync(targetDirectory);
406               resolve(false);
407             });
408         } else if (artifactExt === "zip") {
409           const success = unzipFile(archiveFilePath, targetDirectory);
410           // delete tmp file
411           unlinkSync(archiveFilePath);
412           if (!success) {
413             unlinkSync(targetDirectory);
414           }
415           resolve(success);
416         } else {
417           unlinkSync(archiveFilePath);
418           unlinkSync(targetDirectory);
419           Logger.log(`Error: unknown archive extension: ${artifactExt}`);
420           resolve(false);
421         }
422       });
423
424       response.pipe(fileWriter);
425     }).on("error", () => {
426       // clean
427       unlinkSync(archiveFilePath);
428       unlinkSync(targetDirectory);
429       Logger.log("Error while downloading toolchain.");
430
431       return false;
432     });
433   });
434 }
435
436 export async function downloadAndInstallNinja(
437   version: string,
438   redirectURL?: string
439 ): Promise<boolean> {
440   /*if (process.platform === "linux") {
441     Logger.log("Ninja installation on Linux is not supported.");
442
443     return false;
444   }*/
445
446   const targetDirectory = buildNinjaPath(version);
447
448   // Check if the SDK is already installed
449   if (redirectURL === undefined && existsSync(targetDirectory)) {
450     Logger.log(`Ninja ${version} is already installed.`);
451
452     return true;
453   }
454
455   // Ensure the target directory exists
456   await mkdir(targetDirectory, { recursive: true });
457
458   const tmpBasePath = join(tmpdir(), "pico-sdk");
459   await mkdir(tmpBasePath, { recursive: true });
460   const archiveFilePath = join(tmpBasePath, `ninja.zip`);
461
462   const octokit = new Octokit();
463   // eslint-disable-next-line @typescript-eslint/naming-convention
464   let ninjaAsset: { name: string; browser_download_url: string } | undefined;
465
466   try {
467     if (redirectURL === undefined) {
468       const releaseResponse = await octokit.rest.repos.getReleaseByTag({
469         owner: "ninja-build",
470         repo: "ninja",
471         tag: version,
472       });
473       if (
474         releaseResponse.status !== 200 &&
475         releaseResponse.data === undefined
476       ) {
477         Logger.log(`Error fetching ninja release ${version}.`);
478
479         return false;
480       }
481       const release = releaseResponse.data;
482       const assetName = `ninja-${NINJA_PLATFORMS[process.platform]}.zip`;
483
484       // Find the asset with the name 'ninja-win.zip'
485       ninjaAsset = release.assets.find(asset => asset.name === assetName);
486     } else {
487       ninjaAsset = {
488         name: version,
489         // eslint-disable-next-line @typescript-eslint/naming-convention
490         browser_download_url: redirectURL,
491       };
492     }
493   } catch (error) {
494     Logger.log(
495       `Error fetching ninja release ${version}. ${
496         error instanceof Error ? error.message : (error as string)
497       }`
498     );
499
500     return false;
501   }
502
503   if (!ninjaAsset) {
504     Logger.log(`Error release asset for ninja release ${version} not found.`);
505
506     return false;
507   }
508
509   // Download the asset
510   const assetUrl = ninjaAsset.browser_download_url;
511
512   return new Promise(resolve => {
513     // Use https.get to download the asset
514     get(assetUrl, response => {
515       const code = response.statusCode ?? 404;
516
517       // redirects not supported
518       if (code >= 400) {
519         //return reject(new Error(response.statusMessage));
520         Logger.log("Error while downloading ninja: " + response.statusMessage);
521
522         return resolve(false);
523       }
524
525       // handle redirects
526       if (code > 300 && code < 400 && !!response.headers.location) {
527         return resolve(
528           downloadAndInstallNinja(version, response.headers.location)
529         );
530       }
531
532       // save the file to disk
533       const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
534         // unpack the archive
535         const success = unzipFile(archiveFilePath, targetDirectory);
536
537         // delete tmp file
538         unlinkSync(archiveFilePath);
539
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);
544         }*/
545
546         resolve(success);
547       });
548
549       response.pipe(fileWriter);
550     }).on("error", error => {
551       Logger.log("Error downloading asset:" + error.message);
552       resolve(false);
553     });
554   });
555 }
556
557 /**
558  * Supports Windows and macOS amd64 and arm64.
559  *
560  * @param version
561  * @returns
562  */
563 export async function downloadAndInstallCmake(
564   version: string,
565   redirectURL?: string
566 ): Promise<boolean> {
567   /*if (process.platform === "linux") {
568     Logger.log("CMake installation on Linux is not supported.");
569
570     return false;
571   }*/
572
573   const targetDirectory = buildCMakePath(version);
574
575   // Check if the SDK is already installed
576   if (redirectURL === undefined && existsSync(targetDirectory)) {
577     Logger.log(`CMake ${version} is already installed.`);
578
579     return true;
580   }
581
582   // Ensure the target directory exists
583   await mkdir(targetDirectory, { recursive: true });
584   const assetExt = process.platform === "win32" ? "zip" : "tar.gz";
585
586   const tmpBasePath = join(tmpdir(), "pico-sdk");
587   await mkdir(tmpBasePath, { recursive: true });
588   const archiveFilePath = join(tmpBasePath, `cmake-${version}.${assetExt}`);
589
590   const octokit = new Octokit();
591
592   // eslint-disable-next-line @typescript-eslint/naming-convention
593   let cmakeAsset: { name: string; browser_download_url: string } | undefined;
594
595   try {
596     if (redirectURL === undefined) {
597       const releaseResponse = await octokit.rest.repos.getReleaseByTag({
598         owner: "Kitware",
599         repo: "CMake",
600         tag: version,
601       });
602       if (
603         releaseResponse.status !== 200 &&
604         releaseResponse.data === undefined
605       ) {
606         Logger.log(`Error fetching CMake release ${version}.`);
607
608         return false;
609       }
610       const release = releaseResponse.data;
611       const assetName = `cmake-${version.replace("v", "")}-${
612         CMAKE_PLATFORMS[process.platform]
613       }-${
614         process.platform === "darwin"
615           ? "universal"
616           : process.arch === "arm64"
617           ? process.platform === "linux"
618             ? "aarch64"
619             : "arm64"
620           : "x86_64"
621       }.${assetExt}`;
622
623       cmakeAsset = release.assets.find(asset => asset.name === assetName);
624     } else {
625       cmakeAsset = {
626         name: version,
627         // eslint-disable-next-line @typescript-eslint/naming-convention
628         browser_download_url: redirectURL,
629       };
630     }
631   } catch (error) {
632     Logger.log(
633       `Error fetching CMake release ${version}. ${
634         error instanceof Error ? error.message : (error as string)
635       }`
636     );
637
638     return false;
639   }
640
641   if (!cmakeAsset) {
642     Logger.log(`Error release asset for cmake release ${version} not found.`);
643
644     return false;
645   }
646
647   // Download the asset
648   const assetUrl = cmakeAsset.browser_download_url;
649
650   return new Promise(resolve => {
651     // Use https.get to download the asset
652     get(assetUrl, response => {
653       const code = response.statusCode ?? 0;
654
655       // redirects not supported
656       if (code >= 400) {
657         //return reject(new Error(response.statusMessage));
658         Logger.log(
659           "Error while downloading toolchain: " + response.statusMessage
660         );
661
662         return resolve(false);
663       }
664
665       // handle redirects
666       if (code > 300 && code < 400 && !!response.headers.location) {
667         return resolve(
668           downloadAndInstallCmake(version, response.headers.location)
669         );
670       }
671
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)
677             .then(success => {
678               // delete tmp file
679               unlinkSync(archiveFilePath);
680               // macOS
681               //chmodSync(join(targetDirectory, "CMake.app", "Contents", "bin", "cmake"), 0o755);
682               resolve(success);
683             })
684             .catch(() => {
685               unlinkSync(archiveFilePath);
686               unlinkSync(targetDirectory);
687               resolve(false);
688             });
689         } else if (process.platform === "win32") {
690           const success = unzipFile(archiveFilePath, targetDirectory);
691           // delete tmp file
692           unlinkSync(archiveFilePath);
693           resolve(success);
694         } else {
695           Logger.log(`Error: platform not supported for downloading cmake.`);
696           unlinkSync(archiveFilePath);
697           unlinkSync(targetDirectory);
698
699           resolve(false);
700         }
701       });
702
703       response.pipe(fileWriter);
704     }).on("error", error => {
705       Logger.log("Error downloading asset: " + error.message);
706       resolve(false);
707     });
708   });
709 }
710
711 /**
712  * Only supported Windows amd64 and arm64.
713  *
714  * @returns
715  */
716 export async function downloadEmbedPython(
717   versionBundle: VersionBundle,
718   redirectURL?: string
719 ): Promise<string | undefined> {
720   if (
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")
726   ) {
727     Logger.log(
728       "Embed Python installation on Windows x64 and macOS arm64 only."
729     );
730
731     return;
732   }
733
734   const targetDirectory = buildPython3Path(versionBundle.python.version);
735   const settingsTargetDirectory =
736     `${HOME_VAR}/.pico-sdk` + `/python/${versionBundle.python.version}`;
737
738   // Check if the Embed Python is already installed
739   if (redirectURL === undefined && existsSync(targetDirectory)) {
740     Logger.log(`Embed Python is already installed correctly.`);
741
742     return `${settingsTargetDirectory}/python.exe`;
743   }
744
745   // Ensure the target directory exists
746   await mkdir(targetDirectory, { recursive: true });
747
748   // select download url
749   const downloadUrl = versionBundle.python.windowsAmd64;
750
751   const tmpBasePath = join(tmpdir(), "pico-sdk");
752   await mkdir(tmpBasePath, { recursive: true });
753   const archiveFilePath = join(
754     tmpBasePath,
755     `python-${versionBundle.python.version}.zip`
756   );
757
758   return new Promise(resolve => {
759     const requestOptions = {
760       headers: {
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
764         Accept: "*/*",
765         // eslint-disable-next-line @typescript-eslint/naming-convention
766         "Accept-Encoding": "gzip, deflate, br",
767       },
768     };
769
770     get(downloadUrl, requestOptions, response => {
771       const code = response.statusCode ?? 0;
772
773       if (code >= 400) {
774         //return reject(new Error(response.statusMessage));
775         Logger.log("Error while downloading python: " + response.statusMessage);
776
777         return resolve(undefined);
778       }
779
780       // handle redirects
781       if (code > 300 && code < 400 && !!response.headers.location) {
782         return resolve(
783           downloadEmbedPython(versionBundle, response.headers.location)
784         );
785       }
786
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(
793             archiveFilePath,
794             targetDirectory
795           );
796
797           pkgExtractor
798             .extractPkg()
799             .then(success => {
800               if (versionBundle.python.version.lastIndexOf(".") <= 2) {
801                 Logger.log(
802                   "Error while extracting Python: " +
803                     "Python version has wrong format."
804                 );
805                 resolve(undefined);
806               }
807
808               if (success) {
809                 try {
810                   // create symlink, so the same path can be used as on Windows
811                   const srcPath = joinPosix(
812                     settingsTargetDirectory,
813                     "/Versions/",
814                     versionBundle.python.version.substring(
815                       0,
816                       versionBundle.python.version.lastIndexOf(".")
817                     ),
818                     "bin",
819                     "python3"
820                   );
821                   symlinkSync(
822                     srcPath,
823                     // use .exe as python is already used in the directory
824                     join(settingsTargetDirectory, "python.exe"),
825                     "file"
826                   );
827                   symlinkSync(
828                     srcPath,
829                     // use .exe as python is already used in the directory
830                     join(settingsTargetDirectory, "python3.exe"),
831                     "file"
832                   );
833                 } catch {
834                   resolve(undefined);
835                 }
836
837                 resolve(`${settingsTargetDirectory}/python.exe`);
838               } else {
839                 resolve(undefined);
840               }
841             })
842             .catch(() => {
843               resolve(undefined);
844             });
845         } else {
846           // unpack the archive
847           const success = unzipFile(archiveFilePath, targetDirectory);
848           // delete tmp file
849           unlinkSync(archiveFilePath);
850           resolve(
851             success ? `${settingsTargetDirectory}/python.exe` : undefined
852           );
853         }
854       });
855
856       response.pipe(fileWriter);
857     }).on("error", () => {
858       Logger.log("Error while downloading Embed Python.");
859
860       return false;
861     });
862   });
863 }
864
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";
875
876 /**
877  * Only supported Windows amd64 and macOS arm64 and amd64.
878  *
879  * @returns
880  */
881 export async function downloadGit(
882   redirectURL?: string
883 ): Promise<string | undefined> {
884   if (
885     process.platform !== "win32" ||
886     (process.platform === "win32" && process.arch !== "x64")
887   ) {
888     Logger.log("Git installation on Windows x64 and macOS only.");
889
890     return;
891   }
892
893   const targetDirectory = join(homedir(), ".pico-sdk", "git");
894   const settingsTargetDirectory = `${HOME_VAR}/.pico-sdk/git`;
895
896   // Check if the Embed Python is already installed
897   if (redirectURL === undefined && existsSync(targetDirectory)) {
898     Logger.log(`Git is already installed.`);
899
900     return process.platform === "win32"
901       ? `${settingsTargetDirectory}/cmd/git.exe`
902       : `${settingsTargetDirectory}/bin/git`;
903   }
904
905   // Ensure the target directory exists
906   await mkdir(targetDirectory, { recursive: true });
907
908   // select download url for platform()_arch()
909   const downloadUrl = redirectURL ?? GIT_DOWNLOAD_URL_WIN_AMD64;
910
911   const tmpBasePath = join(tmpdir(), "pico-sdk");
912   await mkdir(tmpBasePath, { recursive: true });
913   const archiveFilePath = join(tmpBasePath, `git.zip`);
914
915   return new Promise(resolve => {
916     const requestOptions = {
917       headers: {
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
921         Accept: "*/*",
922         // eslint-disable-next-line @typescript-eslint/naming-convention
923         "Accept-Encoding": "gzip, deflate, br",
924       },
925     };
926
927     get(downloadUrl, requestOptions, response => {
928       const code = response.statusCode ?? 0;
929
930       if (code >= 400) {
931         //return reject(new Error(response.statusMessage));
932         Logger.log("Error while downloading git: " + response.statusMessage);
933
934         resolve(undefined);
935       }
936
937       // handle redirects
938       if (code > 300 && code < 400 && !!response.headers.location) {
939         return resolve(downloadGit(response.headers.location));
940       }
941
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)
947             .then(success => {
948               unlinkSync(archiveFilePath);
949               resolve(
950                 success ? `${settingsTargetDirectory}/bin/git` : undefined
951               );
952             })
953             .catch(() => {
954               resolve(undefined);
955             });
956         } else {
957           // unpack the archive
958           const success = unzipFile(archiveFilePath, targetDirectory);
959           // delete tmp file
960           unlinkSync(archiveFilePath);
961
962           if (success) {
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
965             exec(
966               `${
967                 process.env.ComSpec === "powershell.exe" ? "&" : ""
968               }"${targetDirectory}/cmd/git.exe" config ` +
969                 `--file "${targetDirectory}/etc/gitconfig" ` +
970                 "--remove-section include",
971               error => {
972                 if (error) {
973                   Logger.log(
974                     `Error executing git: ${
975                       error instanceof Error ? error.message : (error as string)
976                     }`
977                   );
978                   resolve(undefined);
979                 } else {
980                   resolve(`${settingsTargetDirectory}/cmd/git.exe`);
981                 }
982               }
983             );
984           } else {
985             resolve(undefined);
986           }
987         }
988       });
989
990       response.pipe(fileWriter);
991     }).on("error", () => {
992       Logger.log("Error while downloading git.");
993
994       return false;
995     });
996   });
997 }
This page took 0.084842 seconds and 4 git commands to generate.