UNPKG

@readme/nodegit

Version:

Node.js libgit2 asynchronous native bindings

520 lines (434 loc) 15.5 kB
import crypto from "crypto"; import { spawn } from "child_process"; import execPromise from "./execPromise.js"; import got from "got"; import path from "path"; import stream from "stream"; import tar from "tar-fs"; import zlib from "zlib"; import { createWriteStream, promises as fs } from "fs"; import { performance } from "perf_hooks"; import { promisify } from "util"; const pipeline = promisify(stream.pipeline); import packageJson from '../package.json' with { type: "json" }; const OPENSSL_VERSION = "3.0.18"; const win32BatPath = path.join(import.meta.dirname, "build-openssl.bat"); const vendorPath = path.resolve(import.meta.dirname, "..", "vendor"); const opensslPatchPath = path.join(vendorPath, "patches", "openssl"); const extractPath = path.join(vendorPath, "openssl"); const exists = (filePath) => fs.stat(filePath).then(() => true).catch(() => false); const convertArch = (archStr) => { const convertedArch = { 'ia32': 'x86', 'x86': 'x86', 'x64': 'x64', 'arm64': 'arm64' }[archStr]; if (!convertedArch) { throw new Error('unsupported architecture'); } return convertedArch; } const hostArch = convertArch(process.arch); const targetArch = process.env.npm_config_arch ? convertArch(process.env.npm_config_arch) : hostArch; const pathsToIncludeForPackage = [ "include", "lib" ]; const getOpenSSLSourceUrl = (version) => `https://www.openssl.org/source/openssl-${version}.tar.gz`; const getOpenSSLSourceSha256Url = (version) => `${getOpenSSLSourceUrl(version)}.sha256`; class HashVerify extends stream.Transform { constructor(algorithm, onFinal) { super(); this.onFinal = onFinal; this.hash = crypto.createHash(algorithm); } _transform(chunk, encoding, callback) { this.hash.update(chunk, encoding); callback(null, chunk); } _final(callback) { const digest = this.hash.digest("hex"); const onFinalResult = this.onFinal(digest); callback(onFinalResult); } } const makeHashVerifyOnFinal = (expected) => (digest) => { const digestOk = digest === expected; return digestOk ? null : new Error(`Digest not OK: ${digest} !== ${this.expected}`); }; // currently this only needs to be done on linux const applyOpenSSLPatches = async (buildCwd, operatingSystem) => { try { await fs.access(opensslPatchPath); for (const patchFilename of await fs.readdir(opensslPatchPath)) { const patchTarget = patchFilename.split("-")[1]; if (patchFilename.split(".").pop() === "patch" && (patchTarget === operatingSystem || patchTarget === "all")) { console.log(`applying ${patchFilename}`); await execPromise(`patch -up0 -i ${path.join(opensslPatchPath, patchFilename)}`, { cwd: buildCwd }, { pipeOutput: true }); } } } catch(e) { if (e.code === "ENOENT") { // no patches to apply return; } console.log("Patch application failed: ", e); throw e; } } const buildDarwin = async (buildCwd, macOsDeploymentTarget) => { if (!macOsDeploymentTarget) { throw new Error("Expected macOsDeploymentTarget to be specified"); } const buildConfig = targetArch === "x64" ? "darwin64-x86_64-cc" : "darwin64-arm64-cc"; const configureArgs = [ buildConfig, // speed up ecdh on little-endian platforms with 128bit int support "enable-ec_nistp_64_gcc_128", // compile static libraries "no-shared", // disable ssl2, ssl3, and compression "no-ssl2", "no-ssl3", "no-comp", // disable tty ui since it fails a bunch of tests on GHA runners and we're just gonna link anyways "no-ui-console", // set install directory `--prefix="${extractPath}"`, `--openssldir="${extractPath}"`, // set macos version requirement `-mmacosx-version-min=${macOsDeploymentTarget}` ]; await execPromise(`./Configure ${configureArgs.join(" ")}`, { cwd: buildCwd }, { pipeOutput: true }); await applyOpenSSLPatches(buildCwd, "darwin"); // only build the libraries, not the fuzzer or apps await execPromise("make build_libs", { cwd: buildCwd }, { pipeOutput: true }); await execPromise("make test", { cwd: buildCwd }, { pipeOutput: true }); await execPromise("make install_sw", { cwd: buildCwd, maxBuffer: 10 * 1024 * 1024 // we should really just use spawn }, { pipeOutput: true }); }; const buildLinux = async (buildCwd) => { const buildConfig = targetArch === "x64" ? "linux-x86_64" : "linux-aarch64"; const configureArgs = [ buildConfig, // Electron(at least on centos7) imports the libcups library at runtime, which has a // dependency on the system libssl/libcrypto which causes symbol conflicts and segfaults. // To fix this we need to hide all the openssl symbols to prevent them from being overridden // by the runtime linker. // "-fvisibility=hidden", // compile static libraries "no-shared", // disable ssl2, ssl3, and compression "no-ssl2", "no-ssl3", "no-comp", // set install directory `--prefix="${extractPath}"`, `--openssldir="${extractPath}"` ]; await execPromise(`./Configure ${configureArgs.join(" ")}`, { cwd: buildCwd }, { pipeOutput: true }); await applyOpenSSLPatches(buildCwd, "linux"); // only build the libraries, not the fuzzer or apps await execPromise("make build_libs", { cwd: buildCwd }, { pipeOutput: true }); await execPromise("make test", { cwd: buildCwd }, { pipeOutput: true }); // only install software, not the docs await execPromise("make install_sw", { cwd: buildCwd, maxBuffer: 10 * 1024 * 1024 // we should really just use spawn }, { pipeOutput: true }); }; const buildWin32 = async (buildCwd) => { let vcvarsallPath = undefined; if (process.env.npm_config_vcvarsall_path && await exists(process.env.npm_config_vcvarsall_path)) { vcvarsallPath = process.env.npm_config_vcvarsall_path; } else { const potentialMsvsPaths = []; // GYP_MSVS_OVERRIDE_PATH is set by node-gyp so this should cover most cases if (process.env.GYP_MSVS_OVERRIDE_PATH) { potentialMsvsPaths.push(process.env.GYP_MSVS_OVERRIDE_PATH); } const packageTypes = ["BuildTools", "Community", "Professional", "Enterprise"]; const versions = ["2022", "2019"] const computePossiblePaths = (parentPath) => { let possiblePaths = [] for (const packageType of packageTypes) { for (const version of versions) { possiblePaths.push(path.join(parentPath, version, packageType)); } } return possiblePaths; } if (process.env["ProgramFiles(x86)"]) { const parentPath = path.join(process.env["ProgramFiles(x86)"], 'Microsoft Visual Studio'); potentialMsvsPaths.push(...computePossiblePaths(parentPath)); } if (process.env.ProgramFiles) { const parentPath = path.join(process.env.ProgramFiles, 'Microsoft Visual Studio'); potentialMsvsPaths.push(...computePossiblePaths(parentPath)); } for (const potentialPath of potentialMsvsPaths) { const wholePath = path.join(potentialPath, 'VC', 'Auxiliary', 'Build', 'vcvarsall.bat'); console.log("checking", wholePath); if (await exists(wholePath)) { vcvarsallPath = wholePath; break; } } if (!vcvarsallPath) { throw new Error(`vcvarsall.bat not found`); } } let vcTarget; switch (targetArch) { case "x64": vcTarget = "VC-WIN64A"; break; case "x86": vcTarget = "VC-WIN32"; break; case "arm64": vcTarget = "VC-WIN64-ARM"; break; } let vsBuildArch = hostArch === targetArch ? hostArch : `${hostArch}_${targetArch}`; console.log("Using vcvarsall.bat at: ", vcvarsallPath); console.log("Using vsBuildArch: ", vsBuildArch); console.log("Using vcTarget: ", vcTarget); await new Promise((resolve, reject) => { const buildProcess = spawn(`"${win32BatPath}" "${vcvarsallPath}" ${vsBuildArch} ${vcTarget}`, { cwd: buildCwd, shell: process.platform === "win32", env: { ...process.env, NODEGIT_SKIP_TESTS: targetArch !== hostArch ? "1" : undefined } }); buildProcess.stdout.on("data", function(data) { console.info(data.toString().trim()); }); buildProcess.stderr.on("data", function(data) { console.error(data.toString().trim()); }); buildProcess.on("close", function(code) { if (!code) { resolve(); } else { reject(code); } }); }); }; const removeOpenSSLIfOudated = async (openSSLVersion) => { try { let openSSLResult; try { const openSSLPath = path.join(extractPath, "bin", "openssl"); openSSLResult = await execPromise(`${openSSLPath} version`); } catch { /* if we fail to get the version, assume removal not required */ } if (!openSSLResult) { return; } const versionMatch = openSSLResult.match(/^OpenSSL (\d\.\d\.\d[a-z]*)/); const installedVersion = versionMatch && versionMatch[1]; if (!installedVersion || installedVersion === openSSLVersion) { return; } console.log("Removing outdated OpenSSL at: ", extractPath); await fs.rm(extractPath, { recursive: true, force: true }); console.log("Outdated OpenSSL removed."); } catch (err) { console.log("Remove outdated OpenSSL failed: ", err); } }; const makeOnStreamDownloadProgress = () => { let lastReport = performance.now(); return ({ percent, transferred, total }) => { const currentTime = performance.now(); if (currentTime - lastReport > 1 * 1000) { lastReport = currentTime; console.log(`progress: ${transferred}/${total} (${(percent * 100).toFixed(2)}%)`) } }; }; const buildOpenSSLIfNecessary = async ({ macOsDeploymentTarget, openSSLVersion }) => { if (process.platform !== "darwin" && process.platform !== "win32" && process.platform !== "linux") { console.log(`Skipping OpenSSL build, not required on ${process.platform}`); return; } if (process.platform === "linux" && process.env.NODEGIT_OPENSSL_STATIC_LINK !== "1") { console.log(`Skipping OpenSSL build, NODEGIT_OPENSSL_STATIC_LINK !== 1`); return; } await removeOpenSSLIfOudated(openSSLVersion); try { await fs.stat(extractPath); console.log("Skipping OpenSSL build, dir exists"); return; } catch {} const openSSLUrl = getOpenSSLSourceUrl(openSSLVersion); const openSSLSha256Url = getOpenSSLSourceSha256Url(openSSLVersion); const openSSLSha256 = (await got(openSSLSha256Url)).body.trim().split(' ')[0]; const downloadStream = got.stream(openSSLUrl); downloadStream.on("downloadProgress", makeOnStreamDownloadProgress()); await pipeline( downloadStream, new HashVerify("sha256", makeHashVerifyOnFinal(openSSLSha256)), zlib.createGunzip(), tar.extract(extractPath) ); console.log(`OpenSSL ${openSSLVersion} download + extract complete: SHA256 OK.`); const buildCwd = path.join(extractPath, `openssl-${openSSLVersion}`); if (process.platform === "darwin") { await buildDarwin(buildCwd, macOsDeploymentTarget); } else if (process.platform === "linux") { await buildLinux(buildCwd); } else if (process.platform === "win32") { await buildWin32(buildCwd); } else { throw new Error(`Unknown platform: ${process.platform}`); } console.log("Build finished."); } const downloadOpenSSLIfNecessary = async ({ downloadBinUrl, maybeDownloadSha256, maybeDownloadSha256Url }) => { if (process.platform !== "darwin" && process.platform !== "win32" && process.platform !== "linux") { console.log(`Skipping OpenSSL download, not required on ${process.platform}`); return; } if (process.platform === "linux" && process.env.NODEGIT_OPENSSL_STATIC_LINK !== "1") { console.log(`Skipping OpenSSL download, NODEGIT_OPENSSL_STATIC_LINK !== 1`); return; } try { await fs.stat(extractPath); console.log("Skipping OpenSSL download, dir exists"); return; } catch {} if (maybeDownloadSha256Url) { maybeDownloadSha256 = (await got(maybeDownloadSha256Url)).body.trim(); } const downloadStream = got.stream(downloadBinUrl); downloadStream.on("downloadProgress", makeOnStreamDownloadProgress()); const pipelineSteps = [ downloadStream, maybeDownloadSha256 ? new HashVerify("sha256", makeHashVerifyOnFinal(maybeDownloadSha256)) : null, zlib.createGunzip(), tar.extract(extractPath) ].filter(step => step !== null); await pipeline( ...pipelineSteps ); console.log(`OpenSSL download + extract complete${maybeDownloadSha256 ? ": SHA256 OK." : "."}`); console.log("Download finished."); } export const getOpenSSLPackageName = () => { return `openssl-${OPENSSL_VERSION}-${process.platform}-${targetArch}.tar.gz`; } export const getOpenSSLPackagePath = () => path.join(import.meta.dirname, getOpenSSLPackageName()); const getOpenSSLPackageUrl = () => { const hostUrl = new URL(packageJson.binary.host); hostUrl.pathname = getOpenSSLPackageName(); return hostUrl.toString(); }; const buildPackage = async () => { let resolve, reject; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); await pipeline( tar.pack(extractPath, { entries: pathsToIncludeForPackage, ignore: (name) => { // Ignore pkgconfig files return path.extname(name) === ".pc" || path.basename(name) === "pkgconfig"; }, dmode: 0o0755, fmode: 0o0644 }), zlib.createGzip(), new HashVerify("sha256", (digest) => { resolve(digest); }), createWriteStream(getOpenSSLPackagePath()) ); const digest = await promise; await fs.writeFile(`${getOpenSSLPackagePath()}.sha256`, digest); }; const acquireOpenSSL = async () => { try { const downloadBinUrl = process.env.npm_config_openssl_bin_url || (['win32', 'darwin'].includes(process.platform) ? getOpenSSLPackageUrl() : undefined); if (downloadBinUrl && downloadBinUrl !== 'skip' && !process.env.NODEGIT_OPENSSL_BUILD_PACKAGE) { const downloadOptions = { downloadBinUrl }; if (process.env.npm_config_openssl_bin_sha256 !== 'skip') { if (process.env.npm_config_openssl_bin_sha256) { downloadOptions.maybeDownloadSha256 = process.env.npm_config_openssl_bin_sha256; } else { downloadOptions.maybeDownloadSha256Url = `${getOpenSSLPackageUrl()}.sha256`; } } await downloadOpenSSLIfNecessary(downloadOptions); return; } let macOsDeploymentTarget; if (process.platform === "darwin") { macOsDeploymentTarget = process.argv[2] ?? process.env.OPENSSL_MACOS_DEPLOYMENT_TARGET if (!macOsDeploymentTarget || !macOsDeploymentTarget.match(/\d+\.\d+/)) { throw new Error(`Invalid macOsDeploymentTarget: ${macOsDeploymentTarget}`); } } await buildOpenSSLIfNecessary({ openSSLVersion: OPENSSL_VERSION, macOsDeploymentTarget }); if (process.env.NODEGIT_OPENSSL_BUILD_PACKAGE) { await buildPackage(); } } catch (err) { console.error("Acquire failed: ", err); process.exit(1); } }; if (process.argv[1] === import.meta.filename) { try { await acquireOpenSSL(); } catch(error) { console.error("Acquire OpenSSL failed: ", error); process.exit(1); } }