dprint
Version:
Pluggable and configurable code formatting platform written in Rust.
260 lines (236 loc) • 7.52 kB
JavaScript
// @ts-check
;
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;
}
}