Files
meteor/tools/isopackets.js
2015-02-05 16:29:49 -08:00

303 lines
11 KiB
JavaScript

var _ = require('underscore');
var bundler = require('./bundler.js');
var Builder = require('./builder.js');
var buildmessage = require('./buildmessage.js');
var files = require('./files.js');
var compiler = require('./compiler.js');
var config = require('./config.js');
var watch = require('./watch.js');
var Console = require('./console.js').Console;
var isopackCacheModule = require('./isopack-cache.js');
var packageMapModule = require('./package-map.js');
var fiberHelpers = require('./fiber-helpers.js');
// TL;DR: Isopacket is a set of isopacks. Isopackets are used only inside
// meteor-tool.
// An isopacket is a predefined set of isopackages which the meteor command-line
// tool can load into its process. This is how we use the DDP client and many
// other packages inside the tool. The isopackets are listed below in the
// ISOPACKETS constant.
//
// All packages that are in isopackets and all of their transitive dependencies
// must be part of the core Meteor git checkout (not loaded from troposphere).
//
// The requested packages will be loaded together with all of their
// dependencies. If you request to load the same isopacket more than once, you
// will efficiently get the same pre-loaded isopacket. On the other hand, two
// different loaded isopackets contain distinct copies of all of their packages
// copy of all of the packages. The return value is an object that maps package
// name to package exports (that is, it is the Package object from inside the
// sandbox created for the newly loaded packages).
//
// For built releases, all of the isopackets are pre-compiled (as JsImages,
// similar to a plugin or a server program) into the tool.
//
// When run from a checkout, all isopackets are re-compiled early in the startup
// process if any of their sources have changed.
//
// Example usage:
// var DDP = require('./isopackets.js').load('ddp').ddp.DDP;
// var reverse = DDP.connect('reverse.meteor.com');
// Console.info(reverse.call('reverse', 'hello world'));
// All of the defined isopackets. Whenever they are being built, they will be
// built in the order listed here (which is mostly relevant for js-analyze).
var ISOPACKETS = {
// Note: when running from a checkout, js-analyze must always be the
// the first to be rebuilt, because it might need to be loaded as part
// of building other isopackets.
'js-analyze': ['js-analyze'],
'ddp': ['ddp'],
'mongo': ['mongo'],
'ejson': ['ejson'],
'minifiers': ['minifiers'],
'constraint-solver': ['constraint-solver'],
'cordova-support': ['boilerplate-generator', 'logging', 'webapp-hashing',
'xmlbuilder'],
'logging': ['logging']
};
// Caches isopackets in memory (each isopacket only needs to be loaded
// once). This is a map from isopacket name to either:
//
// - The 'Package' dictionary, if the isopacket has already been loaded
// into memory
// - null, if the isopacket hasn't been loaded into memory but its on-disk
// instance is known to be ready
//
// The subtlety here is that when running from a checkout, we don't want to
// accidentally load an isopacket before ensuring that it doesn't need to be
// rebuilt. But we do want to be able to load the js-analyze isopacket as part
// of building other isopackets in ensureIsopacketsLoadable.
var loadedIsopackets = {};
// The main entry point: loads and returns an isopacket from cache or from
// disk. Does not do a build step: ensureIsopacketsLoadable must be called
// first!
var load = function (isopacketName) {
return fiberHelpers.noYieldsAllowed(function () {
if (_.has(loadedIsopackets, isopacketName)) {
if (loadedIsopackets[isopacketName]) {
return loadedIsopackets[isopacketName];
}
// This is the case where the isopacket is up to date on disk but not
// loaded.
var isopacket = loadIsopacketFromDisk(isopacketName);
loadedIsopackets[isopacketName] = isopacket;
return isopacket;
}
if (_.has(ISOPACKETS, isopacketName)) {
throw Error("Can't load isopacket before it has been verified: "
+ isopacketName);
}
throw Error("Unknown isopacket: " + isopacketName);
});
};
var isopacketPath = function (isopacketName) {
return files.pathJoin(config.getIsopacketRoot(), isopacketName);
};
// ensureIsopacketsLoadable is called at startup and ensures that all isopackets
// exist on disk as up-to-date loadable programs.
var calledEnsure = false;
var ensureIsopacketsLoadable = function () {
if (calledEnsure) {
throw Error("can't ensureIsopacketsLoadable twice!");
}
calledEnsure = true;
// If we're not running from checkout, then there's nothing to build and we
// can declare that all isopackets are loadable.
if (! files.inCheckout()) {
_.each(ISOPACKETS, function (packages, name) {
loadedIsopackets[name] = null;
});
return;
}
// We make this object lazily later.
var isopacketBuildContext = null;
var failedPackageBuild = false;
// Look at each isopacket. Check to see if it's on disk and up to date. If
// not, build it. We rebuild them in the order listed in ISOPACKETS, which
// ensures that we deal with js-analyze first.
var messages = Console.withProgressDisplayVisible(function () {
return buildmessage.capture(function () {
_.each(ISOPACKETS, function (packages, isopacketName) {
if (failedPackageBuild)
return;
var isopacketRoot = isopacketPath(isopacketName);
var existingBuildinfo = files.readJSONOrNull(
files.pathJoin(isopacketRoot, 'isopacket-buildinfo.json'));
var needRebuild = ! existingBuildinfo;
if (! needRebuild && existingBuildinfo.builtBy !== compiler.BUILT_BY) {
needRebuild = true;
}
if (! needRebuild) {
var watchSet = watch.WatchSet.fromJSON(existingBuildinfo.watchSet);
if (! watch.isUpToDate(watchSet)) {
needRebuild = true;
}
}
if (! needRebuild) {
// Great, it's loadable without a rebuild.
loadedIsopackets[isopacketName] = null;
return;
}
// We're going to need to build! Make a catalog and loader if we haven't
// yet.
if (! isopacketBuildContext) {
isopacketBuildContext = makeIsopacketBuildContext();
}
buildmessage.enterJob({
title: "bundling " + isopacketName + " packages for the tool"
}, function () {
// Build the packages into the in-memory IsopackCache.
isopacketBuildContext.isopackCache.buildLocalPackages(packages);
if (buildmessage.jobHasMessages())
return;
// Now bundle them into a program.
var built = bundler.buildJsImage({
name: "isopacket-" + isopacketName,
packageMap: isopacketBuildContext.packageMap,
isopackCache: isopacketBuildContext.isopackCache,
use: packages
});
if (buildmessage.jobHasMessages())
return;
var builder = new Builder({outputPath: isopacketRoot});
builder.writeJson('isopacket-buildinfo.json', {
builtBy: compiler.BUILT_BY,
watchSet: built.watchSet.toJSON()
});
built.image.write(builder);
builder.complete();
// It's loadable now.
loadedIsopackets[isopacketName] = null;
});
});
});
});
// This is a build step ... but it's one that only happens in development, so
// it can just crash the app instead of being handled nicely.
if (messages.hasMessages()) {
Console.error("Errors prevented isopacket build:");
Console.printMessages(messages);
throw new Error("isopacket build failed?");
}
};
// Returns a new all-local-packages catalog to be used for building isopackets.
var newIsopacketBuildingCatalog = function () {
if (! files.inCheckout())
throw Error("No need to build isopackets unless in checkout!");
var catalogLocal = require('./catalog-local.js');
var isopacketCatalog = new catalogLocal.LocalCatalog;
var messages = buildmessage.capture(
{ title: "scanning local core packages" },
function () {
// When running from a checkout, isopacket building does use local
// packages, but *ONLY THOSE FROM THE CHECKOUT*: not app packages or
// $PACKAGE_DIRS packages. One side effect of this: we really really
// expect them to all build, and we're fine with dying if they don't
// (there's no worries about needing to springboard).
isopacketCatalog.initialize({
localPackageSearchDirs: [files.pathJoin(
files.getCurrentToolsDir(), 'packages')],
buildingIsopackets: true
});
});
if (messages.hasMessages()) {
Console.arrowError("Errors while scanning core packages:");
Console.printMessages(messages);
throw new Error("isopacket scan failed?");
}
return isopacketCatalog;
};
var makeIsopacketBuildContext = function () {
var context = {};
var catalog = newIsopacketBuildingCatalog();
var versions = {};
_.each(catalog.getAllPackageNames(), function (packageName) {
versions[packageName] = catalog.getLatestVersion(packageName).version;
});
context.packageMap = new packageMapModule.PackageMap(versions, {
localCatalog: catalog
});
// Make an isopack cache that doesn't save isopacks to disk and has no
// access to versioned packages.
context.isopackCache = new isopackCacheModule.IsopackCache({
packageMap: context.packageMap,
includeCordovaUnibuild: false,
// When linking JS files, don't include the padding spaces and line number
// comments. Since isopackets are loaded as part of possibly very short
// 'meteor' tool command invocations, we care more about startup time than
// legibility, and the difference is actually observable (eg 25% speedup
// loading constraint-solver).
noLineNumbers: true
});
return context;
};
// Loads a built isopacket from disk. Always loads (the cache is in 'load', not
// this function). Does not run a build process; it must already be built.
var loadIsopacketFromDisk = function (isopacketName) {
var image = bundler.readJsImage(
files.pathJoin(isopacketPath(isopacketName), 'program.json'));
// An incredibly minimalist version of the environment from
// tools/server/boot.js. Kind of a hack.
var env = {
__meteor_bootstrap__: { startupHooks: [] },
__meteor_runtime_config__: { meteorRelease: "ISOPACKET" }
};
var ret;
var messages = buildmessage.capture({
title: "loading isopacket `" + isopacketName + "`"
}, function () {
ret = image.load(env);
});
// This is a build step ... but it's one that only happens in development, so
// it can just crash the app instead of being handled nicely.
if (messages.hasMessages()) {
Console.error("Errors prevented isopacket load:");
Console.printMessages(messages);
throw new Error("isopacket load failed?");
}
// Run any user startup hooks.
while (env.__meteor_bootstrap__.startupHooks.length) {
var hook = env.__meteor_bootstrap__.startupHooks.shift();
hook();
}
// Setting this to null tells Meteor.startup to call hooks immediately.
env.__meteor_bootstrap__.startupHooks = null;
return ret;
};
var isopackets = exports;
_.extend(exports, {
load: load,
ensureIsopacketsLoadable: ensureIsopacketsLoadable,
ISOPACKETS: ISOPACKETS,
makeIsopacketBuildContext: makeIsopacketBuildContext
});