1 import { createWriteStream, existsSync, rmSync } from "fs";
2 import { mkdir } from "fs/promises";
3 import { homedir, tmpdir } from "os";
4 import { join } from "path";
5 import Logger, { LoggerSource } from "../logger.mjs";
6 import { get } from "https";
7 import { exec } from "child_process";
8 import { HOME_VAR } from "../settings.mjs";
9 import { unxzFile, unzipFile } from "./downloadHelpers.mjs";
10 import { EXT_USER_AGENT } from "./githubREST.mjs";
11 import { unknownErrorToString } from "./errorHelper.mjs";
13 const GIT_DOWNLOAD_URL_WIN_AMD64 =
14 "https://github.com/git-for-windows/git/releases/download" +
15 "/v2.46.0.windows.1/MinGit-2.46.0-64-bit.zip";
18 * Downloads and installs a portable version of Git.
20 * Supports Windows x64 and macOS x64 + amd64.
21 * But currently only execution on Windows x64 is enabled.
23 * @returns The path to the installed Git executable or undefined if the installation failed
25 export async function downloadGit(
27 ): Promise<string | undefined> {
29 process.platform !== "win32" ||
30 (process.platform === "win32" && process.arch !== "x64")
33 LoggerSource.gitDownloader,
34 "Portable Git installation only supported on Windows x64."
40 const targetDirectory = join(homedir(), ".pico-sdk", "git");
41 const settingsTargetDirectory = `${HOME_VAR}/.pico-sdk/git`;
43 // Check if the Embed Python is already installed
44 if (redirectURL === undefined && existsSync(targetDirectory)) {
45 Logger.info(LoggerSource.gitDownloader, `Git is already installed.`);
47 return process.platform === "win32"
48 ? `${settingsTargetDirectory}/cmd/git.exe`
49 : `${settingsTargetDirectory}/bin/git`;
52 // Ensure the target directory exists
53 await mkdir(targetDirectory, { recursive: true });
55 // select download url for platform()_arch()
56 const downloadUrl = redirectURL ?? GIT_DOWNLOAD_URL_WIN_AMD64;
58 const tmpBasePath = join(tmpdir(), "pico-sdk");
59 await mkdir(tmpBasePath, { recursive: true });
60 const archiveFilePath = join(tmpBasePath, `git.zip`);
62 return new Promise(resolve => {
63 const requestOptions = {
65 // eslint-disable-next-line @typescript-eslint/naming-convention
66 "User-Agent": EXT_USER_AGENT,
67 // eslint-disable-next-line @typescript-eslint/naming-convention
69 // eslint-disable-next-line @typescript-eslint/naming-convention
70 "Accept-Encoding": "gzip, deflate, br",
74 get(downloadUrl, requestOptions, response => {
75 const code = response.statusCode ?? 0;
78 //return reject(new Error(response.statusMessage));
80 LoggerSource.gitDownloader,
81 "Downloading git failed:",
82 response.statusMessage ?? "{No status message vailable}."
89 if (code > 300 && code < 400 && !!response.headers.location) {
90 return resolve(downloadGit(response.headers.location));
93 // save the file to disk
94 const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
95 // TODO: maybe remove unused code-path here
96 if (process.platform === "darwin") {
97 unxzFile(archiveFilePath, targetDirectory)
99 rmSync(archiveFilePath, { recursive: true, force: true });
101 success ? `${settingsTargetDirectory}/bin/git` : undefined
108 // unpack the archive
109 const success = unzipFile(archiveFilePath, targetDirectory);
111 rmSync(archiveFilePath, { recursive: true, force: true });
114 // remove include section from gitconfig included in MiniGit
115 // which hardcodes the a path in Programm Files to be used by this git executable
118 process.env.ComSpec === "powershell.exe" ? "&" : ""
119 }"${targetDirectory}/cmd/git.exe" config ` +
120 `--file "${targetDirectory}/etc/gitconfig" ` +
121 "--remove-section include",
125 LoggerSource.gitDownloader,
126 "Executing git failed:",
127 unknownErrorToString(error)
131 resolve(`${settingsTargetDirectory}/cmd/git.exe`);
141 response.pipe(fileWriter);
142 }).on("error", err => {
144 LoggerSource.gitDownloader,
145 "Downloading git failed:",