diff --git a/package-cygwin.js b/package-cygwin.js index 0beea96..9384464 100644 --- a/package-cygwin.js +++ b/package-cygwin.js @@ -1,9 +1,13 @@ +const path = require("path"); + const { generateLinksJson } = require("./scripts/generate-links"); const { consolidateLinks } = require("./scripts/consolidate-links"); if (process.platform === "win32") { + const cygwinPath = path.join(__dirname, ".cygwin"); + // Generate a links.json file, so we know how to restore the hardlinks on unpack - generateLinksJson(); + generateLinksJson(cygwinPath); // Consolidate the links, so we don't pack a bunch of duplicate files! consolidateLinks(); diff --git a/package.json b/package.json index d1f2161..d0099d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "esy-bash", - "version": "0.3.1", + "version": "0.3.6", "description": "Cross-platform bash utilities - primed for Reason/OCaml", "main": "index.js", "bin": { diff --git a/scripts/consolidate-links.js b/scripts/consolidate-links.js index 5d02ba2..181bc99 100644 --- a/scripts/consolidate-links.js +++ b/scripts/consolidate-links.js @@ -1,6 +1,8 @@ const fs = require("fs"); const path = require("path"); +const cp = require("child_process"); +const {bashExec, toCygwinPath} = require("./../bash-exec"); const cygwinFolder = path.join(__dirname, "..", ".cygwin"); @@ -22,7 +24,19 @@ const consolidateLinks = () => { fs.mkdirSync(path.join(cygwinFolder, "_links")); } - const links = readLinks(); + const deleteFile = (file) => { + const fileToDelete = path.join(cygwinFolder, path.normalize(file.trim())); + console.log(`Deleting: ${fileToDelete}`); + if (!fs.existsSync(fileToDelete)) { + console.warn("- Not present: " + fileToDelete); + } else { + fs.unlinkSync(fileToDelete); + } + } + + const allLinks = readLinks(); + const links = allLinks.hardlinks; + // Consolidate hard links Object.keys(links).forEach((key) => { const l = links[key]; @@ -32,20 +46,14 @@ const consolidateLinks = () => { console.log("Dest folder: " + dst); try { - fs.copyFileSync(src, dst); + fs.copyFileSync(src, dst); } catch (ex) { console.error(ex); exit(1); } l.forEach((file) => { - const fileToDelete = path.join(cygwinFolder, path.normalize(file.trim())); - console.log(`Deleting: ${fileToDelete}`); - if (!fs.existsSync(fileToDelete)) { - console.warn("- Not present: " + fileToDelete); - } else { - fs.unlinkSync(fileToDelete); - } + deleteFile(file); }) }); } @@ -64,8 +72,11 @@ const restoreLinks = () => { // Take links as input, and: // Create hardlinks from the '_links' folder to all the relevant binaries - const links = readLinks(); + const allLinks = readLinks(); + // Hydrate hard links + console.log("Hydrating hardlinks..."); + const links = allLinks.hardlinks; Object.keys(links).forEach((key) => { const l = links[key]; @@ -82,6 +93,31 @@ const restoreLinks = () => { } }) }); + + // Hydrate symlinks + console.log("Hydrating symlinks..."); + const symlinks = allLinks.symlinks; + Object.keys(symlinks).forEach((key) => { + const link = path.join(cygwinFolder, key); + const orig = path.join(cygwinFolder, symlinks[key]); + + if (!fs.existsSync(orig)) { + console.warn("Cannot find original path, skipping symlink: " + link); + return; + } + + if (fs.existsSync(link)) { + console.warn("Removing existing file at: " + link); + fs.unlinkSync(link); + } + + console.log(`Linking ${link} to ${orig}`) + const cygLink = toCygwinPath(link); + const cygOrig = toCygwinPath(orig); + cp.spawnSync(path.join(cygwinFolder, "bin", "bash.exe"), ["-lc", `ln -s ${cygOrig} ${cygLink}`]); + }); + + console.log("Links successfully restored."); } module.exports = { diff --git a/scripts/generate-links.js b/scripts/generate-links.js index dd8e5b5..153342d 100644 --- a/scripts/generate-links.js +++ b/scripts/generate-links.js @@ -30,6 +30,62 @@ const getHardLinksForFile = (filePath) => { return lines; }; +const cygwinPath = path.join(__dirname, "..", ".cygwin"); +const bashCommand = path.join(cygwinPath, "bin", "bash.exe"); +const bashExec = (command) => { + let output = cp.execSync(`${bashCommand} -lc "${command}"`); + return output.toString().trim(); +}; + +const cygPath = (p) => { + p = p.split("\\").join("/") + let ret = bashExec(`cygpath -u ${p}`); + return ret; +}; + +const extensionsToIgnore = [ + ".exe", + ".dll", + ".sh", + ".h", + ".hpp", + ".db", + ".gz", + ".c", + ".a" +]; + +const isSymlink = (filePath) => { + // Speed up check by only looking for symlinks with a whitelisted extension + let shouldIgnore = extensionsToIgnore.reduce((prev, curr) => prev || filePath.endsWith(curr), false); + if (shouldIgnore) { + return false; + } + + // HACK: Skip non-ssl and non-etc paths to speed this up... + if (filePath.indexOf("ssl") === -1 && filePath.indexOf("etc") === -1) { + return false; + } + + console.log("Checking symlink: " + filePath); + let isSymlink = true; + try { + let ret = bashExec("test -L " + cygPath(filePath)); + console.log("SYMLINK! " + filePath); + } catch (ex) { + isSymlink = false; + } + + return isSymlink; +}; + +// Helper method to get the symlink contents from a cygwin symlink file +const extractSymlinkFromPath = (filePath) => { + let result = bashExec(`readlink ${cygPath(filePath)}`) + + return result.trim(); +}; + const getAllHardLinks = async (folder, curr) => { console.log("-checking: " + folder); const stats = fs.statSync(folder); @@ -40,16 +96,23 @@ const getAllHardLinks = async (folder, curr) => { } else { const hardLinks = getHardLinksForFile(folder); if(hardLinks.length > 1) { - console.log(" -- found hardlink!"); - curr[hardLinks[0]] = hardLinks; + console.log(" -- found hardlink: " + folder); + curr.hardlinks[hardLinks[0]] = hardLinks; + } else if(isSymlink(folder)) { + let relativePath = convertCygwinPathToRelativePath(path.dirname(folder), path.basename(folder)) + console.log(" -- found symlink: " + relativePath); + curr.symlinks["/" + relativePath] = extractSymlinkFromPath(folder); } } }; -const generateLinksJson = () => { - let ret = {}; - getAllHardLinks(path.join(__dirname, "..", ".cygwin"), ret); - fs.writeFileSync(path.join(__dirname, "..", ".cygwin", "links.json"), JSON.stringify(ret)); +const generateLinksJson = (p) => { + let ret = { + hardlinks: {}, + symlinks: {}, + }; + getAllHardLinks(p, ret); + fs.writeFileSync(path.join(p, "links.json"), JSON.stringify(ret)); }; module.exports = {