UNPKG

esy-bash

Version:

Cross-platform bash utilities - primed for Reason/OCaml

239 lines (194 loc) 7.6 kB
const fs = require("fs"); const path = require("path"); const cp = require("child_process"); const promisify = require("util").promisify; const existsAsync = promisify(fs.exists); const mkdirAsync = promisify(fs.mkdir); const linkAsync = promisify(fs.link); const unlinkAsync = promisify(fs.unlink); const copyFileAsync = promisify(fs.copyFile); const debugLog = (log) => { const isDebug = process.env["ESY_BASH_DEBUG"]; if (isDebug) { console.log("[DEBUG] " + log); } }; const spawnAsync = (command, args, options) => { return new Promise((resolve, reject) => { debugLog(`Running command: ${command} with args ${JSON.stringify(args)}`) let proc = cp.spawn(command, args, options); let data = ""; let err = ""; proc.stdout.on("data", (d) => { data += d; }) proc.stderr.on("data", (e) => { err += e; }); proc.on("close", (exitCode) => { if (exitCode === 0) { debugLog("Command was succesful."); resolve(data); } else { debugLog("Command failed with: " + err); reject(err); } }); }); }; const toCygwinPathAsync = async (p) => { p = p.split("\\").join("/") let ret = await spawnAsync(path.join(rootFolder, "re", "_build", "default", "bin", "EsyBash.exe"), ["bash", "-lc", `cygpath '${p}'`]); return ret.trim(); } const rootFolder = path.join(__dirname, ".."); const cygwinFolder = path.join(__dirname, "..", ".cygwin"); const linksFolder = path.join(cygwinFolder, "_links"); const getNameForKey = (key) => { return key.trim().split("/").join("_s_"); } const readLinks = () => { const linkInfo = fs.readFileSync(path.join(cygwinFolder, "links.json")).toString("utf8"); return JSON.parse(linkInfo); } const consolidateLinks = async () => { // Take the links as input, and: // 1) Move the executables to a 'links' folder // 2) Delete all linked references const linksFolderExists = await existsAsync(linksFolder); if (!linksFolderExists) { await mkdirAsync(linksFolder); } const deleteFile = async (file) => { const fileToDelete = path.join(cygwinFolder, path.normalize(file.trim())); console.log(`Deleting: ${fileToDelete}`); const fileExists = await existsAsync(fileToDelete); if (!fileExists) { console.warn("- Not present: " + fileToDelete); } else { await unlinkAsync(fileToDelete); } } const allLinks = readLinks(); const links = allLinks.hardlinks; // Consolidate hard links let outerPromises = Object.keys(links).map(async (key) => { const l = links[key]; const src = path.join(cygwinFolder, path.normalize(key.trim())); const dst = path.join(cygwinFolder, "_links", getNameForKey(key)); console.log("Source folder: " + src); console.log("Dest folder: " + dst); try { await copyFileAsync(src, dst); } catch (ex) { console.error(ex); exit(1); } let promises = l.map(async (file) => { await deleteFile(file); }) await Promise.all(promises); }); await Promise.all(outerPromises); } const ensureFolder = async (p) => { let exists = await existsAsync(p); if (exists) { return; } await ensureFolder(path.dirname(p)); // TODO: Don't use exists here. // These are moved to sync now until we fix this codepath - // otherwise there is a race condition that hits an error. if (fs.existsSync(p)) { return; } else { fs.mkdirSync(p); } }; const checkUserFolder = async (p) => { const esyFolder = path.join(cygwinFolder, "usr", "esy"); const bashRc = path.join(esyFolder, ".bash_profile"); try { const folderExists = await existsAsync(esyFolder); const bashRcExists = await existsAsync(bashRc); console.log("/usr/esy folder exists: " + folderExists.toString()); console.log("/usr/esy/.bashrc exists: " + bashRcExists.toString()); } catch (ex) { console.warn("/usr/esy " + "folder not set up correctly!") } }; const createSymlink = async (cygOrig, cygLink) => { // We'll use the real bash.exe instead of `EsyBash.exe` for this specific operation - restoring symlinks // These cymlinks were specifically 'cygwin-style' symlinks, and since we pass a `CYGWIN` environment // variable that overrides this in `EsyBash.exe`, we won't get quite the same behavior. // // This is important because it allows us to setup these symlinks w/o needing an administrator environment! await spawnAsync(path.join(rootFolder, ".cygwin", "bin", "bash.exe"), ["-lc", `ln -s "${cygOrig}" "${cygLink}"`], { env: { // We also need to override 'HOME', otherwise // this might be picked up from the environment. // This is also overridden in EsyBash.exe. HOME: "" } }); } const restoreLinks = async () => { // Take links as input, and: // Create hardlinks from the '_links' folder to all the relevant binaries const allLinks = readLinks(); await checkUserFolder(); // Hydrate hard links console.log("Hydrating hardlinks..."); const links = allLinks.hardlinks; let outerPromises = Object.keys(links).map(async (key) => { const l = links[key]; const src = path.join(cygwinFolder, "_links", getNameForKey(key)); let promises = l.map(async (file) => { const dst = path.join(cygwinFolder, file.trim()); await ensureFolder(path.dirname(dst)); const exists = await existsAsync(dst); if (exists) { debugLog("Warning - file already exists: " + dst); } else { await linkAsync(src, dst); } }) await Promise.all(promises); }); await Promise.all(outerPromises); // Hydrate symlinks console.log("Hydrating symlinks..."); const symlinks = allLinks.symlinks; outerPromises = Object.keys(symlinks).map(async (key) => { const link = path.join(cygwinFolder, key); let orig = symlinks[key]; // If the key points to an absolute path, use that. // Otherwise, it's relative to the link, so append the path. if (orig[0] !== '/' && orig[0] !== '\\') { orig = path.dirname(link) + "/" + orig; } else { orig = path.join(cygwinFolder, orig); } const origExists = await existsAsync(orig); if (!origExists) { debugLog("Cannot find original path: " + orig + ", skipping symlink: " + link); return; } const linkExists = await existsAsync(link); if (linkExists) { await unlinkAsync(link); } debugLog(`Linking ${link} to ${orig}`) const cygLink = await toCygwinPathAsync(link); const cygOrig = await toCygwinPathAsync(orig); await createSymlink(cygOrig, cygLink); debugLog(`Successfully linked ${link} to ${orig}`) }); await Promise.all(outerPromises); console.log("Links restored."); } module.exports = { consolidateLinks, restoreLinks };