mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
The options.transformFilename function is only supposed to transform
target file names, not the names of source files to be copied.
This behavior was broken by c5809a4a1c
between Meteor 1.4.0.1 and 1.4.1. Thank goodness for `git bisect`!
The biggest symptom of this mistake was that `meteor create --package`
no longer created files based on the ~fs-name~.js and ~fs-name~-tests.js
template files.
This probably merits a 1.4.1.4 release in addition to 1.4.2.2.
1754 lines
54 KiB
JavaScript
1754 lines
54 KiB
JavaScript
///
|
|
/// utility functions for files and directories. includes both generic
|
|
/// helper functions (such as rm_recursive), and meteor-specific ones
|
|
/// (such as testing whether an directory is a meteor app)
|
|
///
|
|
|
|
var assert = require("assert");
|
|
var fs = require("fs");
|
|
var path = require('path');
|
|
var os = require('os');
|
|
var util = require('util');
|
|
var _ = require('underscore');
|
|
var Fiber = require('fibers');
|
|
var crypto = require('crypto');
|
|
var spawn = require("child_process").spawn;
|
|
|
|
var rimraf = require('rimraf');
|
|
var sourcemap = require('source-map');
|
|
var sourceMapRetrieverStack = require('../tool-env/source-map-retriever-stack.js');
|
|
|
|
var utils = require('../utils/utils.js');
|
|
var cleanup = require('../tool-env/cleanup.js');
|
|
var buildmessage = require('../utils/buildmessage.js');
|
|
var fiberHelpers = require('../utils/fiber-helpers.js');
|
|
var colonConverter = require('../utils/colon-converter.js');
|
|
|
|
var miniFiles = require('./mini-files.js');
|
|
|
|
var Profile = require('../tool-env/profile.js').Profile;
|
|
|
|
// Attach all exports of miniFiles here to avoid code duplication
|
|
var files = exports;
|
|
_.extend(files, miniFiles);
|
|
|
|
var parsedSourceMaps = {};
|
|
var nextStackFilenameCounter = 1;
|
|
|
|
// Use the source maps specified to runJavaScript
|
|
var useParsedSourceMap = function (pathForSourceMap) {
|
|
// Check our fancy source map data structure, used for isopacks
|
|
if (_.has(parsedSourceMaps, pathForSourceMap)) {
|
|
return {map: parsedSourceMaps[pathForSourceMap]};
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// Try this source map first
|
|
sourceMapRetrieverStack.push(useParsedSourceMap);
|
|
|
|
// given a predicate function and a starting path, traverse upwards
|
|
// from the path until we find a path that satisfies the predicate.
|
|
//
|
|
// returns either the path to the lowest level directory that passed
|
|
// the test or null for none found. if starting path isn't given, use
|
|
// cwd.
|
|
var findUpwards = function (predicate, startPath) {
|
|
var testDir = startPath || files.cwd();
|
|
while (testDir) {
|
|
if (predicate(testDir)) {
|
|
break;
|
|
}
|
|
var newDir = files.pathDirname(testDir);
|
|
if (newDir === testDir) {
|
|
testDir = null;
|
|
} else {
|
|
testDir = newDir;
|
|
}
|
|
}
|
|
if (!testDir) {
|
|
return null;
|
|
}
|
|
|
|
return testDir;
|
|
};
|
|
|
|
files.cwd = function () {
|
|
return files.convertToStandardPath(process.cwd());
|
|
};
|
|
|
|
// Determine if 'filepath' (a path, or omit for cwd) is within an app
|
|
// directory. If so, return the top-level app directory.
|
|
files.findAppDir = function (filepath) {
|
|
var isAppDir = function (filepath) {
|
|
// XXX once we are done with the transition to engine, this should
|
|
// change to: `return files.exists(path.join(filepath, '.meteor',
|
|
// 'release'))`
|
|
|
|
// .meteor/packages can be a directory, if .meteor is a warehouse
|
|
// directory. since installing meteor initializes a warehouse at
|
|
// $HOME/.meteor, we want to make sure your home directory (and all
|
|
// subdirectories therein) don't count as being within a meteor app.
|
|
try { // use try/catch to avoid the additional syscall to files.exists
|
|
return files.stat(
|
|
files.pathJoin(filepath, '.meteor', 'packages')).isFile();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
return findUpwards(isAppDir, filepath);
|
|
};
|
|
|
|
files.findPackageDir = function (filepath) {
|
|
var isPackageDir = function (filepath) {
|
|
try {
|
|
return files.stat(files.pathJoin(filepath, 'package.js')).isFile();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
return findUpwards(isPackageDir, filepath);
|
|
};
|
|
|
|
// create a .gitignore file in dirPath if one doesn't exist. add
|
|
// 'entry' to the .gitignore on its own line at the bottom of the
|
|
// file, if the exact line does not already exist in the file.
|
|
files.addToGitignore = function (dirPath, entry) {
|
|
var filepath = files.pathJoin(dirPath, ".gitignore");
|
|
if (files.exists(filepath)) {
|
|
var data = files.readFile(filepath, 'utf8');
|
|
var lines = data.split(/\n/);
|
|
if (_.any(lines, function (x) { return x === entry; })) {
|
|
// already there do nothing
|
|
} else {
|
|
// rewrite file w/ new entry.
|
|
if (data.substr(-1) !== "\n") {
|
|
data = data + "\n";
|
|
}
|
|
data = data + entry + "\n";
|
|
files.writeFile(filepath, data, 'utf8');
|
|
}
|
|
} else {
|
|
// doesn't exist, just write it.
|
|
files.writeFile(filepath, entry + "\n", 'utf8');
|
|
}
|
|
};
|
|
|
|
// Are we running Meteor from a git checkout?
|
|
files.inCheckout = _.once(function () {
|
|
try {
|
|
if (files.exists(files.pathJoin(files.getCurrentToolsDir(), '.git'))) {
|
|
return true;
|
|
}
|
|
} catch (e) { console.log(e); }
|
|
|
|
return false;
|
|
});
|
|
|
|
// True if we are using a warehouse: either installed Meteor, or if
|
|
// $METEOR_WAREHOUSE_DIR is set. Otherwise false (we're in a git checkout and
|
|
// just using packages from the checkout).
|
|
files.usesWarehouse = function () {
|
|
// Test hook: act like we're "installed" using a non-homedir warehouse
|
|
// directory.
|
|
if (process.env.METEOR_WAREHOUSE_DIR) {
|
|
return true;
|
|
} else {
|
|
return ! files.inCheckout();
|
|
}
|
|
};
|
|
|
|
// Read the '.tools_version.txt' file. If in a checkout, throw an error.
|
|
files.getToolsVersion = function () {
|
|
if (! files.inCheckout()) {
|
|
var isopackJsonPath = files.pathJoin(files.getCurrentToolsDir(),
|
|
'..', // get out of tool, back to package
|
|
'isopack.json');
|
|
|
|
var parsed;
|
|
|
|
if (files.exists(isopackJsonPath)) {
|
|
var isopackJson = files.readFile(isopackJsonPath);
|
|
parsed = JSON.parse(isopackJson);
|
|
|
|
// XXX "isopack-1" is duplicate of isopack.currentFormat
|
|
parsed = parsed["isopack-1"]; // get the right format from the JSON
|
|
return parsed.name + '@' + parsed.version;
|
|
}
|
|
|
|
// XXX COMPAT WITH 0.9.3
|
|
var unipackageJsonPath = files.pathJoin(files.getCurrentToolsDir(),
|
|
'..', // get out of tool, back to package
|
|
'unipackage.json');
|
|
var unipackageJson = files.readFile(unipackageJsonPath);
|
|
parsed = JSON.parse(unipackageJson);
|
|
return parsed.name + '@' + parsed.version;
|
|
|
|
} else {
|
|
throw new Error("Unexpected. Git checkouts don't have tools versions.");
|
|
}
|
|
};
|
|
|
|
// Return the root of dev_bundle (probably /usr/local/meteor in an
|
|
// install, or (checkout root)/dev_bundle in a checkout.).
|
|
files.getDevBundle = function () {
|
|
return files.pathJoin(files.getCurrentToolsDir(), 'dev_bundle');
|
|
};
|
|
|
|
files.getCurrentNodeBinDir = function () {
|
|
return files.pathJoin(files.getDevBundle(), "bin");
|
|
}
|
|
|
|
// Return the top-level directory for this meteor install or checkout
|
|
files.getCurrentToolsDir = function () {
|
|
var dirname = files.convertToStandardPath(__dirname);
|
|
return files.pathJoin(dirname, '..', '..');
|
|
};
|
|
|
|
// Read a settings file and sanity-check it. Returns a string on
|
|
// success or null on failure (in which case buildmessages will be
|
|
// emitted).
|
|
files.getSettings = function (filename, watchSet) {
|
|
buildmessage.assertInCapture();
|
|
var absPath = files.pathResolve(filename);
|
|
var buffer = require("./watch.js").readAndWatchFile(watchSet, absPath);
|
|
if (buffer === null) {
|
|
buildmessage.error("file not found (settings file)",
|
|
{ file: filename });
|
|
return null;
|
|
}
|
|
|
|
if (buffer.length > 0x10000) {
|
|
buildmessage.error("settings file is too large (must be less than 64k)",
|
|
{ file: filename });
|
|
return null;
|
|
}
|
|
|
|
var str = buffer.toString('utf8');
|
|
|
|
// Ensure that the string is parseable in JSON, but there's no reason to use
|
|
// the object value of it yet.
|
|
if (str.match(/\S/)) {
|
|
try {
|
|
JSON.parse(str);
|
|
} catch (e) {
|
|
buildmessage.error("parse error reading settings file",
|
|
{ file: filename });
|
|
}
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
// Try to find the prettiest way to present a path to the
|
|
// user. Presently, the main thing it does is replace $HOME with ~.
|
|
files.prettyPath = function (p) {
|
|
p = files.realpath(p);
|
|
var home = files.getHomeDir();
|
|
if (! home) {
|
|
return p;
|
|
}
|
|
var relativeToHome = files.pathRelative(home, p);
|
|
if (relativeToHome.substr(0, 3) === ('..' + files.pathSep)) {
|
|
return p;
|
|
}
|
|
return files.pathJoin('~', relativeToHome);
|
|
};
|
|
|
|
// Like statSync, but null if file not found
|
|
files.statOrNull = function (path) {
|
|
return statOrNull(path);
|
|
};
|
|
|
|
function statOrNull(path, preserveSymlinks) {
|
|
try {
|
|
return preserveSymlinks
|
|
? files.lstat(path)
|
|
: files.stat(path);
|
|
} catch (e) {
|
|
if (e.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Like rm -r.
|
|
files.rm_recursive = Profile("files.rm_recursive", function (p) {
|
|
const path = files.convertToOSPath(p);
|
|
try {
|
|
rimraf.sync(path);
|
|
} catch (e) {
|
|
if (e.code === "ENOTEMPTY" &&
|
|
Fiber.current &&
|
|
Fiber.yield &&
|
|
! Fiber.yield.disallowed) {
|
|
new Promise((resolve, reject) => {
|
|
rimraf(path, err => {
|
|
err ? reject(err) : resolve();
|
|
});
|
|
}).await();
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
// Makes all files in a tree read-only.
|
|
var makeTreeReadOnly = function (p) {
|
|
try {
|
|
// the l in lstat is critical -- we want to ignore symbolic links
|
|
var stat = files.lstat(p);
|
|
} catch (e) {
|
|
if (e.code == "ENOENT") {
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
_.each(files.readdir(p), function (file) {
|
|
makeTreeReadOnly(files.pathJoin(p, file));
|
|
});
|
|
}
|
|
if (stat.isFile()) {
|
|
var permissions = stat.mode & 0o777;
|
|
var readOnlyPermissions = permissions & 0o555;
|
|
if (permissions !== readOnlyPermissions) {
|
|
files.chmod(p, readOnlyPermissions);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Returns the base64 SHA256 of the given file.
|
|
files.fileHash = function (filename) {
|
|
var crypto = require('crypto');
|
|
var hash = crypto.createHash('sha256');
|
|
hash.setEncoding('base64');
|
|
var rs = files.createReadStream(filename);
|
|
return new Promise(function (resolve) {
|
|
rs.on('end', function () {
|
|
rs.close();
|
|
resolve(hash.digest('base64'));
|
|
});
|
|
rs.pipe(hash, { end: false });
|
|
}).await();
|
|
};
|
|
|
|
// This is the result of running fileHash on a blank file.
|
|
files.blankHash = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
|
|
|
|
// Returns a base64 SHA256 hash representing a tree on disk. It is not sensitive
|
|
// to modtime, uid/gid, or any permissions bits other than the current-user-exec
|
|
// bit on normal files.
|
|
files.treeHash = function (root, options) {
|
|
options = _.extend({
|
|
ignore: function (relativePath) {
|
|
return false;
|
|
}
|
|
}, options);
|
|
|
|
var crypto = require('crypto');
|
|
var hash = crypto.createHash('sha256');
|
|
|
|
var hashLog = process.env.TREE_HASH_DEBUG ?
|
|
['\n\nTREE HASH for ' + root + '\n'] : null;
|
|
|
|
var updateHash = function (text) {
|
|
hashLog && hashLog.push(text);
|
|
hash.update(text);
|
|
};
|
|
|
|
var traverse = function (relativePath) {
|
|
if (options.ignore(relativePath)) {
|
|
hashLog && hashLog.push('SKIP ' + JSON.stringify(relativePath) + '\n');
|
|
return;
|
|
}
|
|
|
|
var absPath = files.pathJoin(root, relativePath);
|
|
var stat = files.lstat(absPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
if (relativePath) {
|
|
updateHash('dir ' + JSON.stringify(relativePath) + '\n');
|
|
}
|
|
_.each(files.readdir(absPath), function (entry) {
|
|
traverse(files.pathJoin(relativePath, entry));
|
|
});
|
|
} else if (stat.isFile()) {
|
|
if (!relativePath) {
|
|
throw Error("must call files.treeHash on a directory");
|
|
}
|
|
updateHash('file ' + JSON.stringify(relativePath) + ' ' +
|
|
stat.size + ' ' + files.fileHash(absPath) + '\n');
|
|
if (stat.mode & 0o100) {
|
|
updateHash('exec\n');
|
|
}
|
|
} else if (stat.isSymbolicLink()) {
|
|
if (!relativePath) {
|
|
throw Error("must call files.treeHash on a directory");
|
|
}
|
|
updateHash('symlink ' + JSON.stringify(relativePath) + ' ' +
|
|
JSON.stringify(files.readlink(absPath)) + '\n');
|
|
}
|
|
// ignore anything weirder
|
|
};
|
|
|
|
traverse('');
|
|
hashLog && files.appendFile(process.env.TREE_HASH_DEBUG, hashLog.join(''));
|
|
return hash.digest('base64');
|
|
};
|
|
|
|
// like mkdir -p. if it returns true, the item is a directory (even if
|
|
// it was already created). if it returns false, the item is not a
|
|
// directory and we couldn't make it one.
|
|
files.mkdir_p = function (dir, mode) {
|
|
var p = files.pathResolve(dir);
|
|
var ps = files.pathNormalize(p).split(files.pathSep);
|
|
|
|
var stat = files.statOrNull(p);
|
|
if (stat) {
|
|
return stat.isDirectory();
|
|
}
|
|
|
|
// doesn't exist. recurse to build parent.
|
|
// Don't use files.pathJoin here because it can strip off the leading slash
|
|
// accidentally.
|
|
var parentPath = ps.slice(0, -1).join(files.pathSep);
|
|
var success = files.mkdir_p(parentPath, mode);
|
|
// parent is not a directory.
|
|
if (! success) { return false; }
|
|
|
|
var pathIsDirectory = function (path) {
|
|
var stat = files.statOrNull(path);
|
|
return stat && stat.isDirectory();
|
|
};
|
|
|
|
try {
|
|
files.mkdir(p, mode);
|
|
} catch (err) {
|
|
if (err.code === "EEXIST") {
|
|
if (pathIsDirectory(p)) {
|
|
// all good, someone else created this directory for us while we were
|
|
// yielding
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// double check we exist now
|
|
return pathIsDirectory(p);
|
|
};
|
|
|
|
// Roughly like cp -R.
|
|
//
|
|
// The output files will be readable and writable by everyone that the umask
|
|
// allows, and executable by everyone (modulo umask) if the original file was
|
|
// owner-executable. Symlinks are treated transparently (ie the contents behind
|
|
// them are copied, and it's an error if that points nowhere).
|
|
//
|
|
// If options.transform{Filename, Contents} is present, it should
|
|
// be a function, and the contents (as a buffer) or filename will be
|
|
// passed through the function. Use this to, eg, fill templates.
|
|
//
|
|
// If options.ignore is present, it should be a list of regexps. Any
|
|
// file whose basename matches one of the regexps, before
|
|
// transformation, will be skipped.
|
|
files.cp_r = function(from, to, options = {}) {
|
|
from = files.pathResolve(from);
|
|
|
|
const stat = statOrNull(from, options.preserveSymlinks);
|
|
if (! stat) {
|
|
return;
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
files.mkdir_p(to, 0o755);
|
|
|
|
files.readdir(from).forEach(f => {
|
|
if (options.ignore &&
|
|
_.any(options.ignore,
|
|
pattern => f.match(pattern))) {
|
|
return;
|
|
}
|
|
|
|
const fullFrom = files.pathJoin(from, f);
|
|
|
|
if (options.transformFilename) {
|
|
f = options.transformFilename(f);
|
|
}
|
|
|
|
files.cp_r(
|
|
fullFrom,
|
|
files.pathJoin(to, f),
|
|
options
|
|
);
|
|
})
|
|
|
|
return;
|
|
}
|
|
|
|
files.mkdir_p(files.pathDirname(to));
|
|
|
|
if (stat.isSymbolicLink()) {
|
|
files.symlink(files.readlink(from), to);
|
|
|
|
} else {
|
|
// Create the file as readable and writable by everyone, and
|
|
// executable by everyone if the original file is executable by
|
|
// owner. (This mode will be modified by umask.) We don't copy the
|
|
// mode *directly* because this function is used by 'meteor create'
|
|
// which is copying from the read-only tools tree into a writable app.
|
|
const mode = (stat.mode & 0o100) ? 0o777 : 0o666;
|
|
|
|
if (options.transformContents) {
|
|
files.writeFile(to, options.transformContents(
|
|
files.readFile(from),
|
|
files.pathBasename(from)
|
|
), { mode });
|
|
|
|
} else {
|
|
copyFileHelper(from, to, mode);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get every path in a directory recursively, treating symlinks as files
|
|
* @param {String} dir The directory to walk, either relative to options.cwd or completely absolute
|
|
* @param {Object} options Some options
|
|
* @param {String} options.cwd The directory that paths should be relative to
|
|
* @param {String[]} options.output An array to push results to
|
|
* @return {String[]} All of the paths in the directory recursively
|
|
*/
|
|
files.getPathsInDir = function (dir, options) {
|
|
// Don't let this function yield so that the file system doesn't get changed
|
|
// underneath us
|
|
return fiberHelpers.noYieldsAllowed(function () {
|
|
var cwd = options.cwd || files.convertToStandardPath(process.cwd());
|
|
|
|
if (! files.exists(cwd)) {
|
|
throw new Error("Specified current working directory doesn't exist: " +
|
|
cwd);
|
|
}
|
|
|
|
var absoluteDir = files.pathResolve(cwd, dir);
|
|
|
|
if (! files.exists(absoluteDir)) {
|
|
// There are no paths in this dir, so don't do anything
|
|
return;
|
|
}
|
|
|
|
var output = options.output || [];
|
|
|
|
var pathIsDirectory = function (path) {
|
|
var stat = files.lstat(path);
|
|
return stat.isDirectory();
|
|
};
|
|
|
|
_.each(files.readdir(absoluteDir), function (entry) {
|
|
var newPath = files.pathJoin(dir, entry);
|
|
var newAbsPath = files.pathJoin(absoluteDir, entry);
|
|
|
|
output.push(newPath);
|
|
|
|
if (pathIsDirectory(newAbsPath)) {
|
|
files.getPathsInDir(newPath, {
|
|
cwd: cwd,
|
|
output: output
|
|
});
|
|
}
|
|
});
|
|
|
|
return output;
|
|
});
|
|
};
|
|
|
|
files.findPathsWithRegex = function (dir, regex, options) {
|
|
var allPaths = files.getPathsInDir(dir, {
|
|
cwd: options.cwd
|
|
});
|
|
|
|
return _.filter(allPaths, function (path) {
|
|
return path.match(regex);
|
|
});
|
|
};
|
|
|
|
// Copies a file, which is expected to exist. Parent directories of "to" do not
|
|
// have to exist. Treats symbolic links transparently (copies the contents, not
|
|
// the link itself, and it's an error if the link doesn't point to a file).
|
|
files.copyFile = function (from, to, origMode=null) {
|
|
files.mkdir_p(files.pathDirname(files.pathResolve(to)), 0o755);
|
|
|
|
if (origMode === null) {
|
|
var stats = files.stat(from);
|
|
if (!stats.isFile()) {
|
|
throw Error("cannot copy non-files");
|
|
}
|
|
origMode = stats.mode;
|
|
}
|
|
|
|
// Create the file as readable and writable by everyone, and executable by
|
|
// everyone if the original file is executably by owner. (This mode will be
|
|
// modified by umask.) We don't copy the mode *directly* because this function
|
|
// is used by 'meteor create' which is copying from the read-only tools tree
|
|
// into a writable app.
|
|
var mode = (origMode & 0o100) ? 0o777 : 0o666;
|
|
|
|
copyFileHelper(from, to, mode);
|
|
};
|
|
files.copyFile = Profile("files.copyFile", files.copyFile);
|
|
|
|
var copyFileHelper = function (from, to, mode) {
|
|
var readStream = files.createReadStream(from);
|
|
var writeStream = files.createWriteStream(to, { mode: mode });
|
|
new Promise(function (resolve, reject) {
|
|
readStream.on('error', reject);
|
|
writeStream.on('error', reject);
|
|
writeStream.on('open', function () {
|
|
readStream.pipe(writeStream);
|
|
});
|
|
writeStream.once('finish', resolve);
|
|
}).await();
|
|
};
|
|
|
|
// Make a temporary directory. Returns the path to the newly created
|
|
// directory. Only the current user is allowed to read or write the
|
|
// files in the directory (or add files to it). The directory will
|
|
// be cleaned up on exit.
|
|
var tempDirs = [];
|
|
files.mkdtemp = function (prefix) {
|
|
var make = function () {
|
|
prefix = prefix || 'mt-';
|
|
// find /tmp
|
|
var tmpDir = _.first(_.map(['TMPDIR', 'TMP', 'TEMP'], function (t) {
|
|
return process.env[t];
|
|
}).filter(_.identity));
|
|
|
|
if (! tmpDir && process.platform !== 'win32') {
|
|
tmpDir = '/tmp';
|
|
}
|
|
|
|
if (! tmpDir) {
|
|
throw new Error("Couldn't create a temporary directory.");
|
|
}
|
|
|
|
tmpDir = files.realpath(tmpDir);
|
|
|
|
// make the directory. give it 3 tries in case of collisions from
|
|
// crappy random.
|
|
var tries = 3;
|
|
while (tries > 0) {
|
|
var dirPath = files.pathJoin(
|
|
tmpDir, prefix + (Math.random() * 0x100000000 + 1).toString(36));
|
|
try {
|
|
files.mkdir(dirPath, 0o700);
|
|
return dirPath;
|
|
} catch (err) {
|
|
tries--;
|
|
}
|
|
}
|
|
throw new Error("failed to make temporary directory in " + tmpDir);
|
|
};
|
|
var dir = make();
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
};
|
|
|
|
// Call this if you're done using a temporary directory. It will asynchronously
|
|
// be deleted.
|
|
files.freeTempDir = function (tempDir) {
|
|
if (! _.contains(tempDirs, tempDir)) {
|
|
throw Error("not a tracked temp dir: " + tempDir);
|
|
}
|
|
if (process.env.METEOR_SAVE_TMPDIRS) {
|
|
return;
|
|
}
|
|
setImmediate(function () {
|
|
// note: rm_recursive can yield, so it's possible that during this
|
|
// rm_recursive call, the onExit rm_recursive fires too. (Or it could even
|
|
// start firing before the setImmediate handler is called.) But it should be
|
|
// OK for there to be overlapping rm_recursive calls, since rm_recursive
|
|
// ignores all ENOENT calls. And we don't remove tempDir from tempDirs until
|
|
// it's done, so that if mid-way through this rm_recursive the onExit one
|
|
// fires, it still gets removed.
|
|
|
|
try {
|
|
files.rm_recursive(tempDir);
|
|
} catch (err) {
|
|
// Don't crash and print a stack trace because we failed to delete a temp
|
|
// directory. This happens sometimes on Windows and seems to be
|
|
// unavoidable.
|
|
console.log(err);
|
|
}
|
|
|
|
tempDirs = _.without(tempDirs, tempDir);
|
|
});
|
|
};
|
|
|
|
if (! process.env.METEOR_SAVE_TMPDIRS) {
|
|
cleanup.onExit(function (sig) {
|
|
_.each(tempDirs, function (tempDir) {
|
|
try {
|
|
files.rm_recursive(tempDir);
|
|
} catch (err) {
|
|
// Don't crash and print a stack trace because we failed to delete a temp
|
|
// directory. This happens sometimes on Windows and seems to be
|
|
// unavoidable.
|
|
console.log(err);
|
|
}
|
|
});
|
|
|
|
tempDirs = [];
|
|
});
|
|
}
|
|
|
|
// Takes a buffer containing `.tar.gz` data and extracts the archive
|
|
// into a destination directory. destPath should not exist yet, and
|
|
// the archive should contain a single top-level directory, which will
|
|
// be renamed atomically to destPath. The entire tree will be made
|
|
// readonly.
|
|
files.extractTarGz = function (buffer, destPath, options) {
|
|
var options = options || {};
|
|
var parentDir = files.pathDirname(destPath);
|
|
var tempDir = files.pathJoin(parentDir, '.tmp' + utils.randomToken());
|
|
files.mkdir_p(tempDir);
|
|
|
|
if (! _.has(options, "verbose")) {
|
|
options.verbose = require("../console/console.js").Console.verbose;
|
|
}
|
|
|
|
const startTime = +new Date;
|
|
|
|
let promise = process.platform === "win32"
|
|
? tryExtractWithNative7z(buffer, tempDir, options)
|
|
: tryExtractWithNativeTar(buffer, tempDir, options)
|
|
|
|
promise = promise.catch(
|
|
error => tryExtractWithNpmTar(buffer, tempDir, options)
|
|
);
|
|
|
|
promise.await();
|
|
|
|
// succeed!
|
|
var topLevelOfArchive = files.readdir(tempDir)
|
|
// On Windows, the 7z.exe tool sometimes creates an auxiliary
|
|
// PaxHeader directory.
|
|
.filter(file => ! file.startsWith("PaxHeader"));
|
|
|
|
if (topLevelOfArchive.length !== 1) {
|
|
throw new Error(
|
|
"Extracted archive '" + tempDir + "' should only contain one entry");
|
|
}
|
|
|
|
var extractDir = files.pathJoin(tempDir, topLevelOfArchive[0]);
|
|
makeTreeReadOnly(extractDir);
|
|
files.rename(extractDir, destPath);
|
|
files.rm_recursive(tempDir);
|
|
|
|
if (options.verbose) {
|
|
console.log("Finished extracting in", new Date - startTime, "ms");
|
|
}
|
|
};
|
|
|
|
function ensureDirectoryEmpty(dir) {
|
|
files.readdir(dir).forEach(file => {
|
|
files.rm_recursive(files.pathJoin(dir, file));
|
|
});
|
|
}
|
|
|
|
function tryExtractWithNativeTar(buffer, tempDir, options) {
|
|
ensureDirectoryEmpty(tempDir);
|
|
|
|
if (options.forceConvert) {
|
|
return Promise.reject(new Error(
|
|
"Native tar cannot convert colons in package names"));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const flags = options.verbose ? "-xzvf" : "-xzf";
|
|
const tarProc = spawn("tar", [flags, "-"], {
|
|
cwd: files.convertToOSPath(tempDir),
|
|
stdio: options.verbose ? [
|
|
"pipe", // Always need to write to tarProc.stdin.
|
|
process.stdout,
|
|
process.stderr
|
|
] : "pipe",
|
|
});
|
|
|
|
tarProc.on("error", reject);
|
|
tarProc.on("exit", resolve);
|
|
|
|
tarProc.stdin.write(buffer);
|
|
tarProc.stdin.end();
|
|
});
|
|
}
|
|
|
|
function tryExtractWithNative7z(buffer, tempDir, options) {
|
|
ensureDirectoryEmpty(tempDir);
|
|
|
|
if (options.forceConvert) {
|
|
return Promise.reject(new Error(
|
|
"Native 7z.exe cannot convert colons in package names"));
|
|
}
|
|
|
|
const exeOSPath = files.convertToOSPath(
|
|
files.pathJoin(files.getCurrentNodeBinDir(), "7z.exe"));
|
|
const tarGzBasename = "out.tar.gz";
|
|
const spawnOptions = {
|
|
cwd: files.convertToOSPath(tempDir),
|
|
stdio: options.verbose ? "inherit" : "pipe",
|
|
};
|
|
|
|
files.writeFile(files.pathJoin(tempDir, tarGzBasename), buffer);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
spawn(exeOSPath, [
|
|
"x", "-y", tarGzBasename
|
|
], spawnOptions)
|
|
.on("error", reject)
|
|
.on("exit", resolve);
|
|
|
|
}).then(code => {
|
|
assert.strictEqual(code, 0);
|
|
|
|
let tarBasename;
|
|
const foundTar = files.readdir(tempDir).some(file => {
|
|
if (file !== tarGzBasename) {
|
|
tarBasename = file;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
assert.ok(foundTar, "failed to find .tar file");
|
|
|
|
function cleanUp() {
|
|
files.unlink(files.pathJoin(tempDir, tarGzBasename));
|
|
files.unlink(files.pathJoin(tempDir, tarBasename));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
spawn(exeOSPath, [
|
|
"x", "-y", tarBasename
|
|
], spawnOptions)
|
|
.on("error", reject)
|
|
.on("exit", resolve);
|
|
|
|
}).then(code => {
|
|
cleanUp();
|
|
return code;
|
|
}, error => {
|
|
cleanUp();
|
|
throw error;
|
|
});
|
|
});
|
|
}
|
|
|
|
function tryExtractWithNpmTar(buffer, tempDir, options) {
|
|
ensureDirectoryEmpty(tempDir);
|
|
|
|
var tar = require("tar");
|
|
var zlib = require("zlib");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
var gunzip = zlib.createGunzip().on('error', reject);
|
|
var extractor = new tar.Extract({
|
|
path: files.convertToOSPath(tempDir)
|
|
}).on('entry', function (e) {
|
|
if (process.platform === "win32" || options.forceConvert) {
|
|
// On Windows, try to convert old packages that have colons in
|
|
// paths by blindly replacing all of the paths. Otherwise, we
|
|
// can't even extract the tarball
|
|
e.path = colonConverter.convert(e.path);
|
|
}
|
|
}).on('error', reject)
|
|
.on('end', resolve);
|
|
|
|
// write the buffer to the (gunzip|untar) pipeline; these calls
|
|
// cause the tar to be extracted to disk.
|
|
gunzip.pipe(extractor);
|
|
gunzip.write(buffer);
|
|
gunzip.end();
|
|
});
|
|
}
|
|
|
|
// Tar-gzips a directory, returning a stream that can then be piped as
|
|
// needed. The tar archive will contain a top-level directory named
|
|
// after dirPath.
|
|
files.createTarGzStream = function (dirPath, options) {
|
|
var tar = require("tar");
|
|
var fstream = require('fstream');
|
|
var zlib = require("zlib");
|
|
|
|
// Don't use `{ path: dirPath, type: 'Directory' }` as an argument to
|
|
// fstream.Reader. This triggers a collection of odd behaviors in fstream
|
|
// (which might be bugs or might just be weirdnesses).
|
|
//
|
|
// First, if we pass an object with `type: 'Directory'` as an argument, then
|
|
// the resulting tarball has no entry for the top-level directory, because
|
|
// the reader emits an entry (with just the path, no permissions or other
|
|
// properties) before the pipe to gzip is even set up, so that entry gets
|
|
// lost. Even if we pause the streams until all the pipes are set up, we'll
|
|
// get the entry in the tarball for the top-level directory without
|
|
// permissions or other properties, which is problematic. Just passing
|
|
// `dirPath` appears to cause `fstream` to stat the directory before emitting
|
|
// an entry for it, so the pipes are set up by the time the entry is emitted,
|
|
// and the entry has all the right permissions, etc. from statting it.
|
|
//
|
|
// The second weird behavior is that we need an entry for the top-level
|
|
// directory in the tarball to untar it with npm `tar`. (GNU tar, in
|
|
// contrast, appears to have no problems untarring tarballs without entries
|
|
// for the top-level directory inside them.) The problem is that, without an
|
|
// entry for the top-level directory, `fstream` will create the directory
|
|
// with the same permissions as the first file inside it. This manifests as
|
|
// an EACCESS when untarring if the first file inside the top-level directory
|
|
// is not writeable.
|
|
var fileStream = fstream.Reader({
|
|
path: files.convertToOSPath(dirPath),
|
|
filter: function (entry) {
|
|
if (process.platform !== "win32") {
|
|
return true;
|
|
}
|
|
|
|
// Refuse to create a directory that isn't listable. Tarballs
|
|
// created on Windows will have non-executable directories (since
|
|
// executable isn't a thing in Windows directory permissions), and
|
|
// so the resulting extracted directories will not be listable on
|
|
// Linux/Mac unless we explicitly make them executable. We think
|
|
// this should really be an option that you pass to node tar, but
|
|
// setting it in an 'entry' handler is the same strategy that npm
|
|
// does, so we do that here too.
|
|
if (entry.type === "Directory") {
|
|
entry.mode = (entry.mode || entry.props.mode) | 0o500;
|
|
entry.props.mode = entry.mode;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
var tarStream = fileStream.pipe(tar.Pack({ noProprietary: true }));
|
|
|
|
return tarStream.pipe(zlib.createGzip());
|
|
};
|
|
|
|
// Tar-gzips a directory into a tarball on disk, synchronously.
|
|
// The tar archive will contain a top-level directory named after dirPath.
|
|
files.createTarball = function (dirPath, tarball, options) {
|
|
var out = files.createWriteStream(tarball);
|
|
new Promise(function (resolve, reject) {
|
|
out.on('error', reject);
|
|
out.on('close', resolve);
|
|
files.createTarGzStream(dirPath, options).pipe(out);
|
|
}).await();
|
|
};
|
|
|
|
// Use this if you'd like to replace a directory with another
|
|
// directory as close to atomically as possible. It's better than
|
|
// recursively deleting the target directory first and then
|
|
// renaming. (Failure modes here include "there's a brief moment where
|
|
// toDir does not exist" and "you can end up with garbage directories
|
|
// sitting around", but not "there's any time where toDir exists but
|
|
// is in a state other than initial or final".)
|
|
files.renameDirAlmostAtomically = function (fromDir, toDir) {
|
|
var garbageDir = toDir + '-garbage-' + utils.randomToken();
|
|
|
|
// Get old dir out of the way, if it exists.
|
|
var movedOldDir = true;
|
|
try {
|
|
files.rename(toDir, garbageDir);
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') {
|
|
throw e;
|
|
}
|
|
movedOldDir = false;
|
|
}
|
|
|
|
// Now rename the directory.
|
|
files.rename(fromDir, toDir);
|
|
|
|
// ... and delete the old one.
|
|
if (movedOldDir) {
|
|
files.rm_recursive(garbageDir);
|
|
}
|
|
};
|
|
files.renameDirAlmostAtomically = Profile("files.renameDirAlmostAtomically",
|
|
files.renameDirAlmostAtomically);
|
|
|
|
files.writeFileAtomically = function (filename, contents) {
|
|
const parentDir = files.pathDirname(filename);
|
|
files.mkdir_p(parentDir);
|
|
|
|
const tmpFile = files.pathJoin(
|
|
parentDir,
|
|
'.' + files.pathBasename(filename) + '.' + utils.randomToken()
|
|
);
|
|
|
|
files.writeFile(tmpFile, contents);
|
|
files.rename(tmpFile, filename);
|
|
};
|
|
files.writeFileAtomically = Profile("files.writeFileAtomically",
|
|
files.writeFileAtomically);
|
|
|
|
// Like fs.symlinkSync, but creates a temporay link and renames it over the
|
|
// file; this means it works even if the file already exists.
|
|
// Do not use this function on Windows, it won't work.
|
|
files.symlinkOverSync = function (linkText, file) {
|
|
fiberHelpers.noYieldsAllowed(function () {
|
|
file = files.pathResolve(file);
|
|
var tmpSymlink = files.pathJoin(
|
|
files.pathDirname(file),
|
|
"." + files.pathBasename(file) + ".tmp" + utils.randomToken());
|
|
files.symlink(linkText, tmpSymlink);
|
|
files.rename(tmpSymlink, file);
|
|
});
|
|
};
|
|
|
|
// Return the result of evaluating `code` using
|
|
// `runInThisContext`. `code` will be wrapped in a closure. You can
|
|
// pass additional values to bind in the closure in `options.symbols`,
|
|
// the keys being the symbols to bind and the values being their
|
|
// values. `options.filename` is the filename to use in exceptions
|
|
// that come from inside this code. `options.sourceMap` is an optional
|
|
// source map that represents the file.
|
|
//
|
|
// The really special thing about this function is that if a parse
|
|
// error occurs, we will raise an exception of type
|
|
// files.FancySyntaxError, from which you may read 'message', 'file',
|
|
// 'line', and 'column' attributes ... v8 is normally reluctant to
|
|
// reveal this information but will write it to stderr if you pass it
|
|
// an undocumented flag. Unforunately though node doesn't have dup2 so
|
|
// we can't intercept the write. So instead we use a completely
|
|
// different parser with a better error handling API. Ah well. The
|
|
// underlying V8 issue is:
|
|
// https://code.google.com/p/v8/issues/detail?id=1281
|
|
files.runJavaScript = function (code, options) {
|
|
if (typeof code !== 'string') {
|
|
throw new Error("code must be a string");
|
|
}
|
|
|
|
options = options || {};
|
|
var filename = options.filename || "<anonymous>";
|
|
|
|
return Profile.time('runJavaScript ' + filename, () => {
|
|
|
|
var keys = [], values = [];
|
|
// don't assume that _.keys and _.values are guaranteed to
|
|
// enumerate in the same order
|
|
_.each(options.symbols, function (value, name) {
|
|
keys.push(name);
|
|
values.push(value);
|
|
});
|
|
|
|
var stackFilename = filename;
|
|
if (options.sourceMap) {
|
|
// We want to generate an arbitrary filename that we use to associate the
|
|
// file with its source map.
|
|
stackFilename = "<runJavaScript-" + nextStackFilenameCounter++ + ">";
|
|
}
|
|
|
|
var chunks = [];
|
|
var header = "(function(" + keys.join(',') + "){";
|
|
chunks.push(header);
|
|
if (options.sourceMap) {
|
|
var consumer = new sourcemap.SourceMapConsumer(options.sourceMap);
|
|
chunks.push(sourcemap.SourceNode.fromStringWithSourceMap(
|
|
code, consumer));
|
|
} else {
|
|
chunks.push(code);
|
|
}
|
|
// \n is necessary in case final line is a //-comment
|
|
chunks.push("\n})");
|
|
|
|
var wrapped;
|
|
var parsedSourceMap = null;
|
|
if (options.sourceMap) {
|
|
var node = new sourcemap.SourceNode(null, null, null, chunks);
|
|
var results = node.toStringWithSourceMap({
|
|
file: stackFilename
|
|
});
|
|
wrapped = results.code;
|
|
parsedSourceMap = results.map.toJSON();
|
|
if (options.sourceMapRoot) {
|
|
// Add the specified root to any root that may be in the file.
|
|
parsedSourceMap.sourceRoot = files.pathJoin(
|
|
options.sourceMapRoot, parsedSourceMap.sourceRoot || '');
|
|
}
|
|
// source-map-support doesn't ever look at the sourcesContent field, so
|
|
// there's no point in keeping it in memory.
|
|
delete parsedSourceMap.sourcesContent;
|
|
parsedSourceMaps[stackFilename] = parsedSourceMap;
|
|
} else {
|
|
wrapped = chunks.join('');
|
|
};
|
|
|
|
try {
|
|
// See #runInThisContext
|
|
//
|
|
// XXX it'd be nice to runInNewContext so that the code can't mess
|
|
// with our globals, but objects that come out of runInNewContext
|
|
// have bizarro antimatter prototype chains and break 'instanceof
|
|
// Array'. for now, steer clear
|
|
//
|
|
// Pass 'true' as third argument if we want the parse error on
|
|
// stderr (which we don't).
|
|
var script = require('vm').createScript(wrapped, stackFilename);
|
|
} catch (nodeParseError) {
|
|
if (!(nodeParseError instanceof SyntaxError)) {
|
|
throw nodeParseError;
|
|
}
|
|
// Got a parse error. Unfortunately, we can't actually get the
|
|
// location of the parse error from the SyntaxError; Node has some
|
|
// hacky support for displaying it over stderr if you pass an
|
|
// undocumented third argument to stackFilename, but that's not
|
|
// what we want. See
|
|
// https://github.com/joyent/node/issues/3452
|
|
// for more information. One thing to try (and in fact, what an
|
|
// early version of this function did) is to actually fork a new
|
|
// node to run the code and parse its output. We instead run an
|
|
// entirely different JS parser, from the Babel project, but
|
|
// which at least has a nice API for reporting errors.
|
|
var parse = require('meteor-babel').parse;
|
|
try {
|
|
parse(wrapped, { strictMode: false });
|
|
} catch (parseError) {
|
|
if (typeof parseError.loc !== "object") {
|
|
throw parseError;
|
|
}
|
|
|
|
var err = new files.FancySyntaxError;
|
|
err.message = parseError.message;
|
|
|
|
if (parsedSourceMap) {
|
|
// XXX this duplicates code in computeGlobalReferences
|
|
var consumer2 = new sourcemap.SourceMapConsumer(parsedSourceMap);
|
|
var original = consumer2.originalPositionFor(parseError.loc);
|
|
if (original.source) {
|
|
err.file = original.source;
|
|
err.line = original.line;
|
|
err.column = original.column;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
err.file = filename; // *not* stackFilename
|
|
err.line = parseError.loc.line;
|
|
err.column = parseError.loc.column;
|
|
|
|
// adjust errors on line 1 to account for our header
|
|
if (err.line === 1) {
|
|
err.column -= header.length;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
// What? Node thought that this was a parse error and Babel didn't?
|
|
// Eh, just throw Node's error and don't care too much about the line
|
|
// numbers being right.
|
|
throw nodeParseError;
|
|
}
|
|
|
|
var func = script.runInThisContext();
|
|
|
|
return (buildmessage.markBoundary(func)).apply(null, values);
|
|
});
|
|
};
|
|
|
|
// - message: an error message from the parser
|
|
// - file: filename
|
|
// - line: 1-based
|
|
// - column: 1-based
|
|
files.FancySyntaxError = function () {};
|
|
|
|
files.OfflineError = function (error) {
|
|
this.error = error;
|
|
};
|
|
files.OfflineError.prototype.toString = function () {
|
|
return "[Offline: " + this.error.toString() + "]";
|
|
};
|
|
|
|
// Like files.readdir, but skips entries whose names begin with dots, and
|
|
// converts ENOENT to [].
|
|
files.readdirNoDots = function (path) {
|
|
try {
|
|
var entries = files.readdir(path);
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') {
|
|
return [];
|
|
}
|
|
throw e;
|
|
}
|
|
return _.filter(entries, function (entry) {
|
|
return entry && entry[0] !== '.';
|
|
});
|
|
};
|
|
|
|
// Read a file in line by line. Returns an array of lines to be
|
|
// processed individually. Throws if the file doesn't exist or if
|
|
// anything else goes wrong.
|
|
var getLines = function (file) {
|
|
var buffer = files.readFile(file);
|
|
var lines = exports.splitBufferToLines(buffer);
|
|
|
|
// strip blank lines at the end
|
|
while (lines.length) {
|
|
var line = lines[lines.length - 1];
|
|
if (line.match(/\S/)) {
|
|
break;
|
|
}
|
|
lines.pop();
|
|
}
|
|
|
|
return lines;
|
|
};
|
|
|
|
exports.getLines = getLines;
|
|
|
|
exports.splitBufferToLines = function (buffer) {
|
|
return buffer.toString('utf8').split(/\r*\n\r*/);
|
|
};
|
|
|
|
// Same as `getLines`, but returns [] if the file doesn't exist.
|
|
exports.getLinesOrEmpty = function (file) {
|
|
try {
|
|
return getLines(file);
|
|
} catch (e) {
|
|
if (e && e.code === 'ENOENT') {
|
|
return [];
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
// Returns null if the file does not exist, otherwise returns the parsed JSON in
|
|
// the file. Throws on errors other than ENOENT (including JSON parse failure).
|
|
exports.readJSONOrNull = function (file) {
|
|
try {
|
|
var raw = files.readFile(file, 'utf8');
|
|
} catch (e) {
|
|
if (e && e.code === 'ENOENT') {
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
return JSON.parse(raw);
|
|
};
|
|
|
|
// Trims whitespace & other filler characters of a line in a project file.
|
|
files.trimSpaceAndComments = function (line) {
|
|
var match = line.match(/^([^#]*)#/);
|
|
if (match) {
|
|
line = match[1];
|
|
}
|
|
return files.trimSpace(line);
|
|
};
|
|
|
|
// Trims leading and trailing whilespace in a project file.
|
|
files.trimSpace = function (line) {
|
|
return line.replace(/^\s+|\s+$/g, '');
|
|
};
|
|
|
|
|
|
files.KeyValueFile = function (path) {
|
|
var self = this;
|
|
self.path = path;
|
|
}
|
|
|
|
_.extend(files.KeyValueFile.prototype, {
|
|
set: function (k, v) {
|
|
var self = this;
|
|
|
|
var data = self._readAll() || '';
|
|
var lines = data.split(/\n/);
|
|
|
|
var found = false;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var trimmed = lines[i].trim();
|
|
if (trimmed.indexOf(k + '=') == 0) {
|
|
lines[i] = k + '=' + v;
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) {
|
|
lines.push(k + "=" + v);
|
|
}
|
|
var newdata = lines.join('\n') + '\n';
|
|
files.writeFile(self.path, newdata, 'utf8');
|
|
},
|
|
|
|
_readAll: function () {
|
|
var self = this;
|
|
|
|
if (files.exists(self.path)) {
|
|
return files.readFile(self.path, 'utf8');
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
});
|
|
|
|
files.getHomeDir = function () {
|
|
if (process.platform === "win32") {
|
|
return files.pathDirname(
|
|
files.convertToStandardPath(process.env.METEOR_INSTALLATION));
|
|
} else {
|
|
return process.env.HOME;
|
|
}
|
|
};
|
|
|
|
files.currentEnvWithPathsAdded = function (...paths) {
|
|
const env = {...process.env};
|
|
|
|
let pathPropertyName;
|
|
if (process.platform === "win32") {
|
|
// process.env allows for case insensitive access on Windows, but copying it
|
|
// creates a normal JavaScript object with case sensitive property access.
|
|
// This leads to problems, because we would be adding a PATH property instead
|
|
// of setting Path for instance.
|
|
// We want to make sure we're setting the right property, so we
|
|
// lookup the property name case insensitively ourselves.
|
|
pathPropertyName = _.find(Object.keys(env), (key) => {
|
|
return key.toUpperCase() === 'PATH';
|
|
});
|
|
if (!pathPropertyName) {
|
|
pathPropertyName = 'Path';
|
|
}
|
|
} else {
|
|
pathPropertyName = 'PATH';
|
|
}
|
|
|
|
const convertedPaths = paths.map(path => files.convertToOSPath(path));
|
|
let pathDecomposed = (env[pathPropertyName] || "").split(files.pathOsDelimiter);
|
|
pathDecomposed.unshift(...convertedPaths);
|
|
|
|
env[pathPropertyName] = pathDecomposed.join(files.pathOsDelimiter);
|
|
return env;
|
|
}
|
|
|
|
// add .bat extension to link file if not present
|
|
var ensureBatExtension = function (p) {
|
|
if (p.indexOf(".bat") !== p.length - 4) {
|
|
p = p + ".bat";
|
|
}
|
|
return p;
|
|
};
|
|
|
|
// Windows-only, generates a bat script that calls the destination bat script
|
|
files._generateScriptLinkToMeteorScript = function (scriptLocation) {
|
|
var scriptLocationIsAbsolutePath = scriptLocation.match(/^\//);
|
|
var scriptLocationConverted = scriptLocationIsAbsolutePath
|
|
? files.convertToWindowsPath(scriptLocation)
|
|
: "%~dp0\\" + files.convertToWindowsPath(scriptLocation);
|
|
|
|
var newScript = [
|
|
"@echo off",
|
|
"SETLOCAL",
|
|
"SET METEOR_INSTALLATION=%~dp0%",
|
|
|
|
// always convert to Windows path since this function can also be
|
|
// called on Linux or Mac when we are building bootstrap tarballs
|
|
"\"" + scriptLocationConverted + "\" %*",
|
|
"ENDLOCAL",
|
|
|
|
// always exit with the same exit code as the child script
|
|
"EXIT /b %ERRORLEVEL%",
|
|
|
|
// add a comment with the destination of the link, so it can be read later
|
|
// by files.readLinkToMeteorScript
|
|
"rem " + scriptLocationConverted,
|
|
].join(os.EOL);
|
|
|
|
return newScript;
|
|
};
|
|
|
|
files._getLocationFromScriptLinkToMeteorScript = function (script) {
|
|
var lines = _.compact(script.toString().split('\n'));
|
|
|
|
var scriptLocation = _.last(lines)
|
|
.replace(/^rem /g, '');
|
|
var isAbsolute = true;
|
|
|
|
if (scriptLocation.match(/^%~dp0/)) {
|
|
isAbsolute = false;
|
|
scriptLocation = scriptLocation.replace(/^%~dp0\\?/g, '');
|
|
}
|
|
|
|
if (! scriptLocation) {
|
|
throw new Error('Failed to parse script location from meteor.bat');
|
|
}
|
|
|
|
return files.convertToPosixPath(scriptLocation, ! isAbsolute);
|
|
};
|
|
|
|
files.linkToMeteorScript = function (scriptLocation, linkLocation, platform) {
|
|
platform = platform || process.platform;
|
|
|
|
if (platform === 'win32') {
|
|
// Make a meteor batch script that points to current tool
|
|
|
|
linkLocation = ensureBatExtension(linkLocation);
|
|
scriptLocation = ensureBatExtension(scriptLocation);
|
|
var script = files._generateScriptLinkToMeteorScript(scriptLocation);
|
|
|
|
files.writeFile(linkLocation, script, {encoding: "ascii"});
|
|
} else {
|
|
// Symlink meteor tool
|
|
files.symlinkOverSync(scriptLocation, linkLocation);
|
|
}
|
|
};
|
|
|
|
files.readLinkToMeteorScript = function (linkLocation, platform) {
|
|
platform = platform || process.platform;
|
|
if (platform === 'win32') {
|
|
linkLocation = ensureBatExtension(linkLocation);
|
|
var script = files.readFile(linkLocation);
|
|
return files._getLocationFromScriptLinkToMeteorScript(script);
|
|
} else {
|
|
return files.readlink(linkLocation);
|
|
}
|
|
};
|
|
|
|
// Summary of cross platform file system handling strategy
|
|
|
|
// There are three main pain points for handling files on Windows: slashes in
|
|
// paths, line endings in text files, and colons/invalid characters in paths.
|
|
|
|
// 1. Slashes in file paths
|
|
|
|
// We have decided to store all paths inside the tool as unix-style paths in
|
|
// the style of CYGWIN. This means that all paths have forward slashes on all
|
|
// platforms, and C:\ is converted to /c/ on Windows.
|
|
|
|
// All of the methods in files.js know how to convert from these unixy paths
|
|
// to whatever type of path the underlying system prefers.
|
|
|
|
// The reason we chose this strategy because it was easier to make sure to use
|
|
// files.js everywhere instead of node's fs than to make sure every part of
|
|
// the tool correctly uses system-specific path separators. In addition, there
|
|
// are some parts of the tool where it is very hard to tell which strings are
|
|
// used as URLs and which are used as file paths. In some cases, a string can
|
|
// be used as both, meaning it has to have forward slashes no matter what.
|
|
|
|
// 2. Line endings in text files
|
|
|
|
// We have decided to convert all files read by the tool to Unix-style line
|
|
// endings for the same reasons as slashes above. In many parts of the tool,
|
|
// we assume that '\n' is the line separator, and it can be hard to find all
|
|
// of the places and decide whether it is appropriate to use os.EOL. We do not
|
|
// convert anything on write. We will wait and see if anyone complains.
|
|
|
|
// 3. Colons and other invalid characters in file paths
|
|
|
|
// This is not handled automatically by files.js. You need to be careful to
|
|
// escape any colons in package names, etc, before using a string as a file
|
|
// path.
|
|
|
|
// A helpful file to import for this purpose is colon-converter.js, which also
|
|
// knows how to convert various configuration file formats.
|
|
|
|
// Fibers are disabled by default for files.* operations unless
|
|
// process.env.METEOR_DISABLE_FS_FIBERS parses to a falsy value.
|
|
const YIELD_ALLOWED = !! (
|
|
_.has(process.env, "METEOR_DISABLE_FS_FIBERS") &&
|
|
! JSON.parse(process.env.METEOR_DISABLE_FS_FIBERS));
|
|
|
|
files.fsFixPath = {};
|
|
/**
|
|
* Wrap a function from node's fs module to use the right slashes for this OS
|
|
* and run in a fiber, then assign it to the "files" namespace. Each call
|
|
* creates a files.func that runs asynchronously with Fibers (yielding and
|
|
* until the call is done), unless run outside a Fiber or in noYieldsAllowed, in
|
|
* which case it uses fs.funcSync.
|
|
*
|
|
* Also creates a simpler version on files.fsFixPath.* that just fixes the path
|
|
* and fiberizes the Sync version if possible.
|
|
*
|
|
* @param {String} fsFuncName The name of the node fs function to wrap
|
|
* @param {Number[]} pathArgIndices Indices of arguments that have paths, these
|
|
* arguments will be converted to the correct OS slashes
|
|
* @param {Object} options Some options for lesser-used cases
|
|
* @param {Boolean} options.noErr If true, the callback of the wrapped function
|
|
* doesn't have a first "error" argument, for example in fs.exists.
|
|
* @param {Function} options.modifyReturnValue Pass in a function to modify the
|
|
* return value
|
|
*/
|
|
function wrapFsFunc(fsFuncName, pathArgIndices, options) {
|
|
options = options || {};
|
|
|
|
const fsFunc = fs[fsFuncName];
|
|
const fsFuncSync = fs[fsFuncName + "Sync"];
|
|
|
|
function makeWrapper ({alwaysSync, sync}) {
|
|
function wrapper(...args) {
|
|
for (let j = pathArgIndices.length - 1; j >= 0; --j) {
|
|
const i = pathArgIndices[j];
|
|
args[i] = files.convertToOSPath(args[i]);
|
|
}
|
|
|
|
const canYield = YIELD_ALLOWED &&
|
|
Fiber.current &&
|
|
Fiber.yield &&
|
|
! Fiber.yield.disallowed;
|
|
|
|
const shouldBeSync = alwaysSync || sync;
|
|
// There's some overhead in awaiting a Promise of an async call,
|
|
// vs just doing the sync call, which for a call like "stat"
|
|
// takes longer than the call itself. Different parts of the tool
|
|
// may perform 1,000s or 10,000s of stats each under certain
|
|
// conditions, so we get a nice performance boost from making
|
|
// these calls sync.
|
|
const isQuickie = (fsFuncName === 'stat' ||
|
|
fsFuncName === 'rename' ||
|
|
fsFuncName === 'symlink');
|
|
|
|
if (canYield && shouldBeSync && !isQuickie) {
|
|
const promise = new Promise((resolve, reject) => {
|
|
args.push((err, value) => {
|
|
if (options.noErr) {
|
|
resolve(err);
|
|
} else if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(value);
|
|
}
|
|
});
|
|
|
|
fsFunc.apply(fs, args);
|
|
});
|
|
|
|
const result = promise.await();
|
|
|
|
return options.modifyReturnValue
|
|
? options.modifyReturnValue(result)
|
|
: result;
|
|
|
|
} else if (shouldBeSync) {
|
|
// Should be sync but can't yield: we are not in a Fiber.
|
|
// Run the sync version of the fs.* method.
|
|
const result = fsFuncSync.apply(fs, args);
|
|
return options.modifyReturnValue ?
|
|
options.modifyReturnValue(result) : result;
|
|
|
|
} else if (! sync) {
|
|
// wrapping a plain async version
|
|
let cb = args[args.length - 1];
|
|
if (typeof cb === "function") {
|
|
args.pop();
|
|
} else {
|
|
cb = null;
|
|
}
|
|
|
|
new Promise((resolve, reject) => {
|
|
args.push((err, res) => {
|
|
err ? reject(err) : resolve(res);
|
|
});
|
|
fsFunc.apply(fs, args);
|
|
}).then(res => {
|
|
if (options.modifyReturnValue) {
|
|
res = options.modifyReturnValue(res);
|
|
}
|
|
cb && cb(null, res);
|
|
}, cb);
|
|
|
|
return;
|
|
}
|
|
|
|
throw new Error('unexpected');
|
|
}
|
|
|
|
wrapper.displayName = fsFuncName;
|
|
return wrapper;
|
|
}
|
|
|
|
files[fsFuncName] = Profile('files.' + fsFuncName, makeWrapper({ alwaysSync: true }));
|
|
|
|
files.fsFixPath[fsFuncName] =
|
|
Profile('wrapped.fs.' + fsFuncName, makeWrapper({ sync: false }));
|
|
files.fsFixPath[fsFuncName + 'Sync'] =
|
|
Profile('wrapped.fs.' + fsFuncName + 'Sync', makeWrapper({ sync: true }));
|
|
}
|
|
|
|
wrapFsFunc("writeFile", [0]);
|
|
wrapFsFunc("appendFile", [0]);
|
|
wrapFsFunc("readFile", [0], {
|
|
modifyReturnValue: function (fileData) {
|
|
if (_.isString(fileData)) {
|
|
return files.convertToStandardLineEndings(fileData);
|
|
}
|
|
|
|
return fileData;
|
|
}
|
|
});
|
|
wrapFsFunc("stat", [0]);
|
|
wrapFsFunc("lstat", [0]);
|
|
wrapFsFunc("rename", [0, 1]);
|
|
|
|
// After the outermost files.withCache call returns, the withCacheCache is
|
|
// reset to null so that it does not survive server restarts.
|
|
let withCacheCache = null;
|
|
|
|
files.withCache = Profile("files.withCache", function (fn) {
|
|
const oldCache = withCacheCache;
|
|
withCacheCache = oldCache || Object.create(null);
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
withCacheCache = oldCache;
|
|
}
|
|
});
|
|
|
|
function enableCache(name) {
|
|
const method = files[name];
|
|
|
|
function makeCacheKey(args) {
|
|
var parts = [name];
|
|
|
|
for (var i = 0; i < args.length; ++i) {
|
|
var arg = args[i];
|
|
|
|
if (typeof arg !== "string") {
|
|
// If any of the arguments is not a string, then we won't cache
|
|
// the result of the corresponding file.* method invocation.
|
|
return null;
|
|
}
|
|
|
|
parts.push(arg);
|
|
}
|
|
|
|
return parts.join("\0");
|
|
}
|
|
|
|
files[name] = function (...args) {
|
|
if (withCacheCache) {
|
|
var cacheKey = makeCacheKey(args);
|
|
if (cacheKey && cacheKey in withCacheCache) {
|
|
return withCacheCache[cacheKey];
|
|
}
|
|
}
|
|
|
|
const result = method.apply(files, args);
|
|
|
|
if (withCacheCache && cacheKey !== null) {
|
|
// If cacheKey === null, then we called makeCacheKey above and it
|
|
// failed because one of the arguments was not a string, so we
|
|
// should not try to call makeCacheKey again.
|
|
withCacheCache[cacheKey || makeCacheKey(args)] = result;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
enableCache("readdir");
|
|
enableCache("realpath");
|
|
enableCache("stat");
|
|
enableCache("lstat");
|
|
|
|
// The fs.exists method is deprecated in Node v4:
|
|
// https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
|
|
files.exists =
|
|
files.existsSync = function (path, callback) {
|
|
if (typeof callback === "function") {
|
|
throw new Error("Passing a callback to files.exists is no longer supported");
|
|
}
|
|
return !! files.statOrNull(path);
|
|
};
|
|
|
|
if (process.platform === "win32") {
|
|
var rename = files.rename;
|
|
|
|
files.rename = function (from, to) {
|
|
// retries are necessarily only on Windows, because the rename call can fail
|
|
// with EBUSY, which means the file is "busy"
|
|
var maxTries = 10;
|
|
var success = false;
|
|
while (! success && maxTries-- > 0) {
|
|
try {
|
|
rename(from, to);
|
|
success = true;
|
|
} catch (err) {
|
|
if (err.code !== 'EPERM') {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
if (! success) {
|
|
files.cp_r(from, to);
|
|
files.rm_recursive(from);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Warning: doesn't convert slashes in the second 'cache' arg
|
|
wrapFsFunc("realpath", [0], {
|
|
modifyReturnValue: files.convertToStandardPath
|
|
});
|
|
|
|
wrapFsFunc("readdir", [0], {
|
|
modifyReturnValue: function (entries) {
|
|
return _.map(entries, files.convertToStandardPath);
|
|
}
|
|
});
|
|
|
|
wrapFsFunc("rmdir", [0]);
|
|
wrapFsFunc("mkdir", [0]);
|
|
wrapFsFunc("unlink", [0]);
|
|
wrapFsFunc("chmod", [0]);
|
|
wrapFsFunc("open", [0]);
|
|
|
|
// XXX this doesn't give you the second argument to the callback
|
|
wrapFsFunc("read", []);
|
|
wrapFsFunc("write", []);
|
|
wrapFsFunc("close", []);
|
|
wrapFsFunc("symlink", [0, 1]);
|
|
wrapFsFunc("readlink", [0]);
|
|
|
|
// These don't need to be Fiberized
|
|
files.createReadStream = function (...args) {
|
|
args[0] = files.convertToOSPath(args[0]);
|
|
return fs.createReadStream(...args);
|
|
};
|
|
|
|
files.createWriteStream = function (...args) {
|
|
args[0] = files.convertToOSPath(args[0]);
|
|
return fs.createWriteStream(...args);
|
|
};
|
|
|
|
files.watchFile = function (...args) {
|
|
args[0] = files.convertToOSPath(args[0]);
|
|
return fs.watchFile(...args);
|
|
};
|
|
|
|
files.unwatchFile = function (...args) {
|
|
args[0] = files.convertToOSPath(args[0]);
|
|
return fs.unwatchFile(...args);
|
|
};
|
|
|
|
files.readBufferWithLengthAndOffset = function (filename, length, offset) {
|
|
var data = new Buffer(length);
|
|
// Read the data from disk, if it is non-empty. Avoid doing IO for empty
|
|
// files, because (a) unnecessary and (b) fs.readSync with length 0
|
|
// throws instead of acting like POSIX read:
|
|
// https://github.com/joyent/node/issues/5685
|
|
if (length > 0) {
|
|
var fd = files.open(filename, "r");
|
|
try {
|
|
var count = files.read(
|
|
fd, data, 0, length, offset);
|
|
} finally {
|
|
files.close(fd);
|
|
}
|
|
if (count !== length) {
|
|
throw new Error("couldn't read entire resource");
|
|
}
|
|
}
|
|
return data;
|
|
};
|