UNPKG

dprint

Version:

Pluggable and configurable code formatting platform written in Rust.

260 lines (236 loc) 7.52 kB
// @ts-check "use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); /** @type {string | undefined} */ let cachedIsMusl = undefined; module.exports = { replaceBinEntry, runInstall() { const dprintFileName = os.platform() === "win32" ? "dprint.exe" : "dprint"; const targetExecutablePath = path.join( __dirname, dprintFileName, ); if (fs.existsSync(targetExecutablePath)) { return targetExecutablePath; } const target = getTarget(); const sourcePackagePath = path.dirname(require.resolve("@dprint/" + target + "/package.json")); const sourceExecutablePath = path.join(sourcePackagePath, dprintFileName); if (!fs.existsSync(sourceExecutablePath)) { throw new Error("Could not find executable for @dprint/" + target + " at " + sourceExecutablePath); } try { if (process.env.DPRINT_SIMULATED_READONLY_FILE_SYSTEM === "1") { console.warn("Simulating readonly file system for testing."); throw new Error("Throwing for testing purposes."); } // in order to make things faster the next time we run and to allow the // dprint vscode extension to easily pick this up, copy the executable // into the dprint package folder hardLinkOrCopy(sourceExecutablePath, targetExecutablePath); if (os.platform() !== "win32") { // chmod +x chmodX(targetExecutablePath); } return targetExecutablePath; } catch (err) { // this may fail on readonly file systems... in this case, fall // back to using the resolved package path if (process.env.DPRINT_DEBUG === "1") { console.warn( "Failed to copy executable from " + sourceExecutablePath + " to " + targetExecutablePath + ". Using resolved package path instead.", err, ); } return sourceExecutablePath; } }, }; /** @filePath {string} */ function chmodX(filePath) { const fd = fs.openSync(filePath, "r"); try { const perms = fs.fstatSync(fd).mode; fs.fchmodSync(fd, perms | 0o111); } finally { fs.closeSync(fd); } } function getTarget() { const platform = os.platform(); if (platform === "linux") { return platform + "-" + getArch() + "-" + getLinuxFamily(); } else { return platform + "-" + getArch(); } } function getArch() { const arch = os.arch(); if (arch !== "arm64" && arch !== "x64" && arch !== "riscv64" && arch !== "loong64") { throw new Error("Unsupported architecture " + os.arch() + ". Only x64, aarch64, riscv64 and loong64 binaries are available."); } return arch; } function getLinuxFamily() { return getIsMusl() ? "musl" : "glibc"; function getIsMusl() { // code adapted from https://github.com/lovell/detect-libc // Copyright Apache 2.0 license, the detect-libc maintainers if (cachedIsMusl == null) { cachedIsMusl = innerGet(); } return cachedIsMusl; function innerGet() { try { if (os.platform() !== "linux") { return false; } return isProcessReportMusl() || isConfMusl(); } catch (err) { // just in case console.warn("Error checking if musl.", err); return false; } } function isProcessReportMusl() { if (!process.report) { return false; } const rawReport = process.report.getReport(); const report = typeof rawReport === "string" ? JSON.parse(rawReport) : rawReport; if (!report || !(report.sharedObjects instanceof Array)) { return false; } return report.sharedObjects.some(o => o.includes("libc.musl-") || o.includes("ld-musl-")); } function isConfMusl() { const output = getCommandOutput(); const [_, ldd1] = output.split(/[\r\n]+/); return ldd1 && ldd1.includes("musl"); } function getCommandOutput() { try { const command = "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true"; return require("child_process").execSync(command, { encoding: "utf8" }); } catch (_err) { return ""; } } } } /** * Replaces the bin entry in node_modules/.bin to point directly at the * native binary, avoiding Node.js startup overhead on each invocation. * @param exePath {string} */ function replaceBinEntry(exePath) { const binDir = findBinDir(); if (binDir === undefined) return; const relative = path.relative(binDir, exePath); if (os.platform() === "win32") { // rewrite .cmd and .ps1 wrappers to invoke the native binary directly fs.writeFileSync( path.join(binDir, "dprint.cmd"), "@\"%~dp0" + relative + "\" %*\r\n", ); fs.writeFileSync( path.join(binDir, "dprint.ps1"), "& \"$PSScriptRoot/" + relative.replace(/\\/g, "/") + "\" $args\r\nexit $LASTEXITCODE\r\n", ); } else { // replace symlink to point directly at the native binary const binDprint = path.join(binDir, "dprint"); fs.unlinkSync(binDprint); fs.symlinkSync(relative, binDprint); } } function findBinDir() { // For global installs, npm sets npm_config_global=true and npm_config_prefix // to the install prefix. The bin dir is {prefix}/bin on Linux/Mac or // {prefix} on Windows (e.g. %APPDATA%\npm). if (process.env.npm_config_global === "true") { const prefix = process.env.npm_config_prefix; if (prefix) { const binDir = os.platform() === "win32" ? prefix : path.join(prefix, "bin"); if (isBinDirForThisPackage(binDir)) { return binDir; } } } // For local installs, walk up looking for node_modules/.bin let dir = __dirname; for (let i = 0; i < 64; i++) { const parent = path.dirname(dir); if (parent === dir) { break; } if (path.basename(parent) === "node_modules") { const binDir = path.join(parent, ".bin"); if (isBinDirForThisPackage(binDir)) { return binDir; } } dir = parent; } return undefined; } function isBinDirForThisPackage(binDir) { try { if (os.platform() === "win32") { // verify the .cmd wrapper references our bin.cjs const content = fs.readFileSync( path.join(binDir, "dprint.cmd"), "utf8", ); return content.includes("bin.cjs"); } else { // verify the symlink points into our package directory const linkTarget = fs.readlinkSync(path.join(binDir, "dprint")); const resolved = path.resolve(binDir, linkTarget); return resolved.endsWith("bin.cjs"); } } catch (_err) { return false; } } /** * @param sourcePath {string} * @param destinationPath {string} */ function hardLinkOrCopy(sourcePath, destinationPath) { try { fs.linkSync(sourcePath, destinationPath); } catch { atomicCopyFile(sourcePath, destinationPath); } } /** * @param sourcePath {string} * @param destinationPath {string} */ function atomicCopyFile(sourcePath, destinationPath) { const crypto = require("crypto"); const rand = crypto.randomBytes(4).toString("hex"); const tempFilePath = destinationPath + "." + rand; fs.copyFileSync(sourcePath, tempFilePath); try { fs.renameSync(tempFilePath, destinationPath); } catch (err) { // will maybe throw when another process had already done this // so just ignore and delete the created temporary file try { fs.unlinkSync(tempFilePath); } catch (_err2) { // ignore } throw err; } }