Files
meteor/tools/utils.js
David Greenspan e072382438 Kill PV.parseVersionConstraint
It parses “=1.0.0” but not “=1.0.0 || 2.0.0”.  It shouldn’t be a public thing (and is never used publicly).

There should be only two things: versions and constraints.  A value that can legally be “=1.0.0” is not a version, it is a constraint (and yet we still call it versionString in a few places).  A value that can be “=1.0.0” but not “1.0.0 || 2.0.0” is not a constraint; it is at best a rare creature called something like a “simple constraint.”  We may expand the syntax of constraints again in the future, and we can’t have N things called a “constraint.”
2015-01-07 16:15:10 -08:00

660 lines
20 KiB
JavaScript

var Future = require('fibers/future');
var _ = require('underscore');
var fiberHelpers = require('./fiber-helpers.js');
var archinfo = require('./archinfo.js');
var buildmessage = require('./buildmessage.js');
var files = require('./files.js');
var packageVersionParser = require('./package-version-parser.js');
var semver = require('semver');
var os = require('os');
var url = require('url');
var utils = exports;
// Parses <protocol>://<host>:<port> into an object { protocol: *, host:
// *, port: * }. The input can also be of the form <host>:<port> or just
// <port>. We're not simply using 'url.parse' because we want '3000' to
// parse as {host: undefined, protocol: undefined, port: '3000'}, whereas
// 'url.parse' would give us {protocol:' 3000', host: undefined, port:
// undefined} or something like that.
//
// 'defaults' is an optional object with 'host', 'port', and 'protocol' keys.
var parseUrl = function (str, defaults) {
// XXX factor this out into a {type: host/port}?
defaults = defaults || {};
var defaultHost = defaults.host || undefined;
var defaultPort = defaults.port || undefined;
var defaultProtocol = defaults.protocol || undefined;
if (str.match(/^[0-9]+$/)) { // just a port
return {
port: str,
host: defaultHost,
protocol: defaultProtocol };
}
var hasScheme = exports.hasScheme(str);
if (! hasScheme) {
str = "http://" + str;
}
var parsed = url.parse(str);
if (! parsed.protocol.match(/\/\/$/)) {
// For easy concatenation, add double slashes to protocols.
parsed.protocol = parsed.protocol + "//";
}
return {
protocol: hasScheme ? parsed.protocol : defaultProtocol,
host: parsed.hostname || defaultHost,
port: parsed.port || defaultPort
};
};
var ipAddress = function () {
var netroute = require('netroute');
var info = netroute.getInfo();
var defaultRoute = _.findWhere(info.IPv4 || [], { destination: "0.0.0.0" });
if (! defaultRoute) {
return null;
}
var iface = defaultRoute["interface"];
var getAddress = function (iface) {
var interfaces = os.networkInterfaces();
return _.findWhere(interfaces[iface], { family: "IPv4" });
};
var address = getAddress(iface);
if (! address) {
// Retry after a couple seconds in case the user is connecting or
// disconnecting from the Internet.
utils.sleepMs(2000);
address = getAddress(iface);
if (! address) {
throw new Error(
"Interface '" + iface + "' not found in interface list, or\n" +
"does not have an IPv4 address.");
}
}
return address.address;
};
exports.hasScheme = function (str) {
return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//);
};
exports.parseUrl = parseUrl;
exports.ipAddress = ipAddress;
exports.hasScheme = function (str) {
return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//);
};
// XXX: Move to e.g. formatters.js?
// Prints a package list in a nice format.
// Input is an array of objects with keys 'name' and 'description'.
exports.printPackageList = function (items, options) {
options = options || {};
var rows = _.map(items, function (item) {
var name = item.name;
var description = item.description || 'No description';
return [name, description];
});
var alphaSort = function (row) {
return row[0];
};
rows = _.sortBy(rows, alphaSort);
var Console = require('./console.js').Console;
return Console.printTwoColumns(rows, options);
};
// Determine a human-readable hostname for this computer. Prefer names
// that make sense to users (eg, the name they manually gave their
// computer on OS X, which might contain spaces) over names that have
// any particular technical significance (eg, might resolve in DNS).
exports.getHost = function () {
var ret;
var attempt = function () {
var output = files.run.apply(null, arguments);
if (output) {
ret = output.trim();
}
};
if (archinfo.matches(archinfo.host(), 'os.osx')) {
// On OSX, to get the human-readable hostname that the user chose,
// we call:
// scutil --get ComputerName
// This can contain spaces. See
// http://osxdaily.com/2012/10/24/set-the-hostname-computer-name-and-bonjour-name-separately-in-os-x/
if (! ret) attempt("scutil", "--get", "ComputerName");
}
if (archinfo.matches(archinfo.host(), 'os.osx') ||
archinfo.matches(archinfo.host(), 'os.linux')) {
// On Unix-like platforms, try passing -s to hostname to strip off
// the domain name, to reduce the extent to which the output
// varies with DNS.
if (! ret) attempt("hostname", "-s");
}
// Try "hostname" on any platform. It should work on
// Windows. Unknown platforms that have a command called "hostname"
// that deletes all of your files deserve what the get.
if (! ret) attempt("hostname");
// Otherwise, see what Node can come up with.
return ret || os.hostname();
};
// Return standard info about this user-agent. Used when logging in to
// Meteor Accounts, mostly so that when the user is seeing a list of
// their open sessions in their profile on the web, they have a way to
// decide which ones they want to revoke.
exports.getAgentInfo = function () {
var ret = {};
var host = utils.getHost();
if (host)
ret.host = host;
ret.agent = "Meteor";
ret.agentVersion =
files.inCheckout() ? "checkout" : files.getToolsVersion();
ret.arch = archinfo.host();
return ret;
};
// Wait for 'ms' milliseconds, and then return. Yields. (Must be
// called within a fiber, and blocks only the calling fiber, not the
// whole program.)
exports.sleepMs = function (ms) {
if (ms <= 0)
return;
var fut = new Future;
setTimeout(function () { fut['return']() }, ms);
fut.wait();
};
// Return a short, high entropy string without too many funny
// characters in it.
exports.randomToken = function () {
return (Math.random() * 0x100000000 + 1).toString(36);
};
// Returns a random non-privileged port number.
exports.randomPort = function () {
return 20000 + Math.floor(Math.random() * 10000);
};
// Like packageVersionParser.parseConstraint, but if called in a buildmessage
// context uses buildmessage to raise errors.
exports.parseConstraint = function (constraintString, options) {
try {
return packageVersionParser.parseConstraint(constraintString);
} catch (e) {
if (! (e.versionParserError && options && options.useBuildmessage))
throw e;
buildmessage.error(e.message, { file: options.buildmessageFile });
return null;
}
};
exports.validatePackageName = function (name, options) {
try {
return packageVersionParser.validatePackageName(name, options);
} catch (e) {
if (! (e.versionParserError && options && options.useBuildmessage))
throw e;
buildmessage.error(e.message, { file: options.buildmessageFile });
return null;
}
};
// Returns true if the parsed constraint was just a@b with no `=` or `||`.
exports.isSimpleConstraint = function (parsedConstraint) {
return parsedConstraint.constraints.length === 1 &&
parsedConstraint.constraints[0].type === "compatible-with";
};
// Check for invalid package names. Currently package names can only contain
// ASCII alphanumerics, dash, and dot, and must contain at least one letter. For
// safety reasons, package names may not start with a dot. Package names must be
// lowercase.
//
// These do not check that the package name is valid in terms of our naming
// scheme: ie, that it is prepended by a user's username. That check should
// happen at publication time.
//
// 3 variants: isValidPackageName just returns a bool. validatePackageName
// throws an error marked with 'versionParserError'. validatePackageNameOrExit
// (which should only be used inside the implementation of a command, not
// eg package-client.js) prints and throws the "exit with code 1" exception
// on failure.
exports.isValidPackageName = function (packageName) {
try {
exports.validatePackageName(packageName);
return true;
} catch (e) {
if (!e.versionParserError)
throw e;
return false;
}
};
exports.validatePackageNameOrExit = function (packageName, options) {
try {
exports.validatePackageName(packageName, options);
} catch (e) {
if (!e.versionParserError)
throw e;
var Console = require('./console.js').Console;
Console.error(e.message, Console.options({ bulletPoint: "Error: " }));
// lazy-load main: old bundler tests fail if you add a circular require to
// this file
var main = require('./main.js');
throw new main.ExitWithCode(1);
}
};
// True if this looks like a valid email address. We deliberately
// don't support
// - quoted usernames (eg, "foo"@bar.com, " "@bar.com, "@"@bar.com)
// - IP addresses in domains (eg, foo@1.2.3.4 or the IPv6 equivalent)
// because they're weird and we don't want them in our database.
exports.validEmail = function (address) {
return /^[^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/.test(address);
};
// Like Perl's quotemeta: quotes all regexp metacharacters. See
// https://github.com/substack/quotemeta/blob/master/index.js
exports.quotemeta = function (str) {
return String(str).replace(/(\W)/g, '\\$1');
};
// Allow a simple way to scale up all timeouts from the command line
var timeoutScaleFactor = 1.0;
if (process.env.TIMEOUT_SCALE_FACTOR) {
timeoutScaleFactor = parseFloat(process.env.TIMEOUT_SCALE_FACTOR);
}
exports.timeoutScaleFactor = timeoutScaleFactor;
// If the given version matches a template (essentially, semver-style, but with
// a bounded number of digits per number part, and with no restriction on the
// amount of number parts, and some restrictions on legal prerelease labels),
// then return an orderKey for it. Otherwise return null.
//
// This conventional orderKey pads each part (with 0s for numbers, and ! for
// prerelease tags), and appends a $. (Because ! sorts before $, this means that
// the prerelease for a given release will sort before it. Because $ sorts
// before '.', this means that 1.2 will sort before 1.2.3.)
exports.defaultOrderKeyForReleaseVersion = function (v) {
var m = v.match(/^(\d{1,4}(?:\.\d{1,4})*)(?:-([-A-Za-z.]{1,15})(\d{0,4}))?$/);
if (!m)
return null;
var numberPart = m[1];
var prereleaseTag = m[2];
var prereleaseNumber = m[3];
var hasRedundantLeadingZero = function (x) {
return x.length > 1 && x[0] === '0';
};
var leftPad = function (chr, len, str) {
if (str.length > len)
throw Error("too long to pad!");
var padding = new Array(len - str.length + 1).join(chr);
return padding + str;
};
var rightPad = function (chr, len, str) {
if (str.length > len)
throw Error("too long to pad!");
var padding = new Array(len - str.length + 1).join(chr);
return str + padding;
};
// Versions must have no redundant leading zeroes, or else this encoding would
// be ambiguous.
var numbers = numberPart.split('.');
if (_.any(numbers, hasRedundantLeadingZero))
return null;
if (prereleaseNumber && hasRedundantLeadingZero(prereleaseNumber))
return null;
// First, put together the non-prerelease part.
var ret = _.map(numbers, _.partial(leftPad, '0', 4)).join('.');
if (!prereleaseTag)
return ret + '$';
ret += '!' + rightPad('!', 15, prereleaseTag);
if (prereleaseNumber)
ret += leftPad('0', 4, prereleaseNumber);
return ret + '$';
};
// XXX should be in files.js
exports.isDirectory = function (dir) {
try {
// use stat rather than lstat since symlink to dir is OK
var stats = files.stat(dir);
} catch (e) {
return false;
}
return stats.isDirectory();
};
// XXX from Underscore.String (http://epeli.github.com/underscore.string/)
exports.startsWith = function(str, starts) {
return str.length >= starts.length &&
str.substring(0, starts.length) === starts;
};
// Options: noPrefix: do not display 'Meteor ' in front of the version number.
exports.displayRelease = function (track, version, options) {
var catalog = require('./catalog.js');
options = options || {};
var prefix = options.noPrefix ? "" : "Meteor ";
if (track === catalog.DEFAULT_TRACK)
return prefix + version;
return track + '@' + version;
};
exports.splitReleaseName = function (releaseName) {
var parts = releaseName.split('@');
var track, version;
if (parts.length === 1) {
var catalog = require('./catalog.js');
track = catalog.DEFAULT_TRACK;
version = parts[0];
} else {
track = parts[0];
// Do we forbid '@' sign in release versions? I sure hope so, but let's
// be careful.
version = parts.slice(1).join("@");
}
return [track, version];
};
// Calls cb with each subset of the array "total", with non-decreasing size,
// until all subsets have been used or cb returns true. The array passed
// to cb may be safely mutated or retained by cb.
exports.generateSubsetsOfIncreasingSize = function (total, cb) {
// We'll throw this if cb ever returns true, which is a simple way to pop us
// out of our recursion.
var Done = function () {};
// Generates all subsets of size subsetSize which contain the indices already
// in chosenIndices (and no indices that are "less than" any of them).
var generateSubsetsOfFixedSize = function (goalSize, chosenIndices) {
// If we've found a subset of the size we're looking for, output it.
if (chosenIndices.length === goalSize) {
// Change from indices into the actual elements. Note that 'elements' is
// a newly allocated array which cb may mutate or retain.
var elements = [];
_.each(chosenIndices, function (index) {
elements.push(total[index]);
});
if (cb(elements)) {
throw new Done(); // unwind all the recursion
}
return;
}
// Otherwise try adding another index and call this recursively. We're
// trying to produce a sorted list of indices, so if there are already
// indices, we start with the one after the biggest one we already have.
var firstIndexToConsider = chosenIndices.length ?
chosenIndices[chosenIndices.length - 1] + 1 : 0;
for (var i = firstIndexToConsider; i < total.length; ++i) {
var withThisChoice = _.clone(chosenIndices);
withThisChoice.push(i);
generateSubsetsOfFixedSize(goalSize, withThisChoice);
}
};
try {
for (var goalSize = 0; goalSize <= total.length; ++goalSize) {
generateSubsetsOfFixedSize(goalSize, []);
}
} catch (e) {
if (!(e instanceof Done))
throw e;
}
};
exports.isUrlWithSha = function (x) {
// For now, just support http/https, which is at least less restrictive than
// the old "github only" rule.
return /^https?:\/\/.*[0-9a-f]{40}/.test(x);
};
// If there is a version that isn't exact, throws an Error with a
// human-readable message that is suitable for showing to the user.
// dependencies may be falsey or empty.
//
// This is talking about NPM/Cordova versions specifically, not Meteor versions.
// It does not support the wrap number syntax.
exports.ensureOnlyExactVersions = function (dependencies) {
_.each(dependencies, function (version, name) {
// We want a given version of a smart package (package.js +
// .npm/npm-shrinkwrap.json) to pin down its dependencies precisely, so we
// don't want anything too vague. For now, we support semvers and urls that
// name a specific commit by SHA.
if (! exports.isExactVersion(version)) {
throw new Error(
"Must declare exact version of dependency: " + name + '@' + version);
}
});
};
exports.isExactVersion = function (version) {
return semver.valid(version) || exports.isUrlWithSha(version);
};
exports.execFileSync = function (file, args, opts) {
var future = new Future;
var child_process = require('child_process');
var eachline = require('eachline');
if (opts && opts.pipeOutput) {
var p = child_process.spawn(file, args, opts);
eachline(p.stdout, fiberHelpers.bindEnvironment(function (line) {
process.stdout.write(line + '\n');
}));
eachline(p.stderr, fiberHelpers.bindEnvironment(function (line) {
process.stderr.write(line + '\n');
}));
p.on('exit', function (code) {
future.return(code);
});
return {
success: !future.wait(),
stdout: "",
stderr: ""
};
}
child_process.execFile(file, args, opts, function (err, stdout, stderr) {
future.return({
success: ! err,
stdout: stdout,
stderr: stderr
});
});
return future.wait();
};
exports.execFileAsync = function (file, args, opts) {
opts = opts || {};
var child_process = require('child_process');
var eachline = require('eachline');
var p = child_process.spawn(file, args, opts);
var mapper = opts.lineMapper || _.identity;
var logOutput = fiberHelpers.bindEnvironment(function (line) {
if (opts.verbose) {
line = mapper(line);
if (line)
console.log(line);
}
});
eachline(p.stdout, logOutput);
eachline(p.stderr, logOutput);
return p;
};
exports.Throttled = function (options) {
var self = this;
options = _.extend({ interval: 150 }, options || {});
self.interval = options.interval;
var now = +(new Date);
self.next = now;
};
_.extend(exports.Throttled.prototype, {
isAllowed: function () {
var self = this;
var now = +(new Date);
if (now < self.next) {
return false;
}
self.next = now + self.interval;
return true;
}
});
// ThrottledYield just regulates the frequency of calling yield.
// It should behave similarly to calling yield on every iteration of a loop,
// except that it won't actually yield if there hasn't been a long enough time interval
//
// options:
// interval: minimum interval of time between yield calls
// (more frequent calls are simply dropped)
exports.ThrottledYield = function (options) {
var self = this;
self._throttle = new exports.Throttled(options);
};
_.extend(exports.ThrottledYield.prototype, {
yield: function () {
var self = this;
if (self._throttle.isAllowed()) {
var f = new Future;
// setImmediate allows signals and IO to be processed but doesn't
// otherwise add time-based delays. It is better for yielding than
// process.nextTick (which doesn't allow signals or IO to be processed) or
// setTimeout 1 (which adds a minimum of 1 ms and often more in delays).
// XXX Actually, setImmediate is so fast that we might not even need
// to use the throttler at all?
setImmediate(function () {
f.return();
});
f.wait();
}
}
});
// Are we running on device?
exports.runOnDevice = function (options) {
return !! _.intersection(options.args,
['ios-device', 'android-device']).length;
};
// Given the options for a 'meteor run' command, returns a parsed URL ({
// host: *, protocol: *, port: * }. The rules for --mobile-server are:
// * If you don't specify anything for --mobile-server, then it
// defaults to <detected ip address>:<port from --port>.
// * If you specify something for --mobile-server, we use that,
// defaulting to http:// as the protocol and 80 or 443 as the port.
exports.mobileServerForRun = function (options) {
// we want to do different IP generation depending on whether we
// are running for a device or simulator
options = _.extend({}, options, {
runOnDevice: exports.runOnDevice(options)
});
var parsedUrl = parseUrl(options.port);
if (! parsedUrl.port) {
throw new Error("--port must include a port.");
}
// XXX COMPAT WITH 0.9.2.2 -- the 'mobile-port' option is deprecated
var mobileServer = options["mobile-server"] || options["mobile-port"];
// if we specified a mobile server, use that
if (mobileServer) {
var parsedMobileServer = parseUrl(mobileServer, {
protocol: "http://"
});
if (! parsedMobileServer.host) {
throw new Error("--mobile-server must specify a hostname.");
}
return parsedMobileServer;
}
// if we are running on a device, use the auto-detected IP
if (options.runOnDevice) {
var myIp = ipAddress();
if (! myIp) {
throw new Error(
"Error detecting IP address for mobile app to connect to.\n" +
"Please specify the address that the mobile app should connect\n" +
"to with --mobile-server.");
}
return {
host: myIp,
port: parsedUrl.port,
protocol: "http://"
};
}
// we are running a simulator, use localhost:3000
return {
host: "localhost",
port: parsedUrl.port,
protocol: "http://"
};
};
exports.escapePackageNameForPath = function (packageName) {
return packageName.replace(":", "_");
};
exports.unescapePackageNameForPath = function (escapedPackageName) {
return escapedPackageName.replace("_", ":");
};