Files
meteor/tools/library.js
Nick Martin e7bb166a02 Check for invalid package names early in the codepath.
We already don't work with packages that contain '.' in the name, this just moves the error up and makes it clearer.
2013-08-06 15:24:06 -07:00

475 lines
17 KiB
JavaScript

var path = require('path');
var _ = require('underscore');
var files = require('./files.js');
var watch = require('./watch.js');
var packages = require('./packages.js');
var warehouse = require('./warehouse.js');
var bundler = require('./bundler.js');
var buildmessage = require('./buildmessage.js');
var fs = require('fs');
// Under the hood, packages in the library (/packages/foo), and user
// applications, are both Packages -- they are just represented
// differently on disk.
// Options:
// - releaseManifest: a parsed release manifest
// - localPackageDirs: array of directories to search before checking
// the manifest and the warehouse. Directories that don't exist (or
// paths that aren't directories) will be silently ignored.
var Library = function (options) {
var self = this;
options = options || {};
self.releaseManifest = options.releaseManifest;
// Trim down localPackageDirs to just those that actually exist (and
// that are actually directories)
self.localPackageDirs = _.filter(options.localPackageDirs, function (dir) {
try {
// use stat rather than lstat since symlink to dir is OK
var stats = fs.statSync(dir);
} catch (e) {
return false;
}
return stats.isDirectory();
});
self.overrides = {}; // package name to package directory
// both map from package name to:
// - pkg: cached Package object
// - packageDir: directory from which it was loaded
self.softReloadCache = {};
self.loadedPackages = {};
};
_.extend(Library.prototype, {
// Temporarily add a package to the library (or override a package
// that actually exists in the library.) `packageName` is the name
// to use for the package and `packageDir` is the directory that
// contains its source. For now, it is an error to try to install
// two overrides for the same packageName.
override: function (packageName, packageDir) {
var self = this;
if (_.has(self.overrides, packageName))
throw new Error("Duplicate override for package '" + packageName + "'");
self.overrides[packageName] = path.resolve(packageDir);
},
// Undo an override previously set up with override().
removeOverride: function (packageName) {
var self = this;
if (!_.has(self.overrides, packageName))
throw new Error("No override present for package '" + packageName + "'");
delete self.loadedPackages[packageName];
delete self.overrides[packageName];
delete self.softReloadCache[packageName];
},
// Force reload of changed packages. See description at get().
//
// If soft is false, the default, the cache is totally flushed and
// all packages are reloaded unconditionally.
//
// If soft is true, then built packages without dependency info (such as those
// from the warehouse) aren't reloaded (there's no way to rebuild them, after
// all), and if we loaded a built package with dependency info, we won't
// reload it if the dependency info says that its source files are still up to
// date. The ideas is that assuming the user is "following the rules", this
// will correctly reload any changed packages while in most cases avoiding
// nearly all reloading.
refresh: function (soft) {
var self = this;
soft = soft || false;
self.softReloadCache = soft ? self.loadedPackages : {};
self.loadedPackages = {};
},
// Given a package name as a string, returns the absolute path to the package
// directory (which is the *source* tree in the source-with-built-unipackage
// case, not the .build directory), or null if not found.
//
// Does NOT load the package or make any recursive calls, so can safely be
// called from Package initialization code. Intended primarily for comparison
// to the packageDirForBuildInfo field on a Package object; also used
// internally to implement 'get'.
findPackageDirectory: function (name) {
var self = this;
// Packages cached from previous calls
if (_.has(self.loadedPackages, name)) {
return self.loadedPackages[name].packageDir;
}
// If there's an override for this package, use that without
// looking at any other options.
if (_.has(self.overrides, name))
return self.overrides[name];
for (var i = 0; i < self.localPackageDirs.length; ++i) {
var packageDir = path.join(self.localPackageDirs[i], name);
// A directory is a package if it either contains 'package.js' (a package
// source tree) or 'unipackage.json' (a compiled unipackage). (Actually,
// for now, unipackages contain a dummy package.js too.)
//
// XXX support for putting unipackages in a local package dir is
// incomplete! They will be properly loaded, but other packages that
// depend on them have no way of knowing when they change! unipackages
// that are the .build of a source tree work fine (they have a
// buildinfo.json and can be rebuilt), and warehouse unipackages work fine
// too (users are not supposed to edit them (they are read-only on disk),
// and their pathname specifies a version). But if you, eg, have a
// unipackage of coffeescript in a local package directory, build another
// package dependending on it, and substitute another version of the
// unipackage in the same location, nothing will ever rebuild your
// package!
if (fs.existsSync(path.join(packageDir, 'package.js')) ||
fs.existsSync(path.join(packageDir, 'unipackage.json'))) {
return packageDir;
}
}
// Try the Meteor distribution, if we have one.
var version = self.releaseManifest && self.releaseManifest.packages[name];
if (version) {
packageDir = path.join(warehouse.getWarehouseDir(),
'packages', name, version);
// The warehouse is theoretically constructed carefully enough that the
// directory really should not exist unless it is complete.
if (! fs.existsSync(packageDir))
throw new Error("Package missing from warehouse: " + name +
" version " + version);
return packageDir;
}
// Nope!
return null;
},
// Given a package name as a string, retrieve a Package object. If
// throwOnError is true, the default, throw an error if the package
// can't be found. (If false is passed for throwOnError, then return
// null if the package can't be found.) When called inside
// buildmessage.enterJob, however, instead of throwing an error it
// will record a build error and return a dummy (empty) package.
//
// Searches overrides first, then any localPackageDirs you have
// provided, then the manifest/warehouse if provided.
//
// get() caches the packages it returns, meaning if you call
// get('foo') and later foo changes on disk, you won't see the
// changes. To flush the package cache and force all of the packages
// to be reloaded the next time get() is called for them, see
// refresh().
get: function (name, throwOnError) {
var self = this;
// Passed a Package?
if (name instanceof packages.Package)
return name;
// Packages cached from previous calls
if (_.has(self.loadedPackages, name)) {
return self.loadedPackages[name].pkg;
}
// Check for invalid package names.
//
// XXX should we be even stricter and whitelist something like
// /\-_A-Za-z0-9/ instead of blacklisting some special characters?
// What about unicode package names?
if (/[\.\?|'"#<>\(\)]/.test(name)) {
if (throwOnError === false)
return null;
throw new Error("Invalid package name: " + name);
}
var packageDir = self.findPackageDirectory(name);
if (! packageDir) {
if (throwOnError === false)
return null;
buildmessage.error("package not available: " + name);
// recover by returning a dummy (empty) package
var pkg = new packages.Package(self);
pkg.initEmpty(name);
return pkg;
}
// See if we can reuse a package that we have cached from before
// the last soft refresh.
if (_.has(self.softReloadCache, name)) {
var entry = self.softReloadCache[name];
// Either we will decide that the cache is invalid, or we will "upgrade"
// this entry into loadedPackages. Either way, it's not needed in
// softReloadCache any more.
delete self.softReloadCache[name];
if (entry.packageDir === packageDir && entry.pkg.checkUpToDate()) {
// Cache hit
self.loadedPackages[name] = entry;
return entry.pkg;
}
}
// Load package from disk
var pkg = new packages.Package(self, packageDir);
if (fs.existsSync(path.join(packageDir, 'unipackage.json'))) {
// It's an already-built package
pkg.initFromUnipackage(name, packageDir);
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
} else {
// It's a source tree. Does it have a built unipackage inside it?
var buildDir = path.join(packageDir, '.build');
if (fs.existsSync(buildDir) &&
pkg.initFromUnipackage(name, buildDir,
{ onlyIfUpToDate: true,
buildOfPath: packageDir })) {
// We already had a build and it was up to date.
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
} else {
// Either we didn't have a build, or it was out of date. Build the
// package.
buildmessage.enterJob({
title: "building package `" + name + "`",
rootPath: packageDir
}, function () {
// This has to be done in the right sequence: initialize
// (which loads the dependency list but does not get() those
// packages), then put the package into the package list,
// then call build() to get() the dependencies and finish
// the build. If you called build() before putting the
// package in the package list then you'd recurse
// forever. (build() needs the dependencies because it needs
// to look at the handlers registered by any plugins in the
// packages that we use.)
pkg.initFromPackageDir(name, packageDir);
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
pkg.build();
if (! buildmessage.jobHasMessages() && // ensure no errors!
pkg.canBeSavedAsUnipackage()) {
// Save it, for a fast load next time
try {
files.add_to_gitignore(packageDir, '.build*');
pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir });
} catch (e) {
// If we can't write to this directory, we don't get to cache our
// output, but otherwise life is good.
if (!(e && (e.code === 'EACCES' || e.code === 'EPERM')))
throw e;
}
}
});
}
}
return pkg;
},
// Get a package that represents an app. (ignoreFiles is optional
// and if given, it should be an array of regexps for filenames to
// ignore when scanning for source files.)
getForApp: function (appDir, ignoreFiles) {
var self = this;
var pkg = new packages.Package(self);
pkg.initFromAppDir(appDir, ignoreFiles || []);
pkg.build();
return pkg;
},
// Given a slice set spec -- either a package name like "ddp", or a
// particular slice within the package like "ddp.client" -- return
// the list of matching slices (as an array of Slice objects) for a
// given architecture.
getSlices: function (spec, arch) {
var self = this;
var parts = spec.split('.');
if (parts.length === 1) {
var pkg = self.get(parts[0], true);
return pkg.getDefaultSlices(arch);
}
else if (parts.length === 2) {
var pkg = self.get(parts[0], true);
return [pkg.getSingleSlice(parts[1], arch)];
}
else {
// XXX figure out if this is user-visible and if so, improve the
// message
throw new Error("Bad slice spec");
}
},
// Register local package directories with a watchSet. We want to know if a
// package is created or deleted, which includes both its top-level source
// directory and its main package metadata file.
watchLocalPackageDirs: function (watchSet) {
var self = this;
_.each(self.localPackageDirs, function (packageDir) {
var packages = watch.readAndWatchDirectory(watchSet, {
absPath: packageDir,
include: [/\/$/]
});
_.each(packages, function (p) {
watch.readAndWatchFile(watchSet,
path.join(packageDir, p, 'package.js'));
watch.readAndWatchFile(watchSet,
path.join(packageDir, p, 'unipackage.json'));
});
});
},
// Get all packages available. Returns a map from the package name
// to a Package object.
//
// XXX Hack: If errors occur while generating the list (which could
// easily happen, since it currently involves building packages)
// print them to the console and exit(1)! Certainly not ideal but is
// expedient since, eg, test-packages calls list() before it does
// anything else.
list: function () {
var self = this;
var names = [];
var ret = {};
var messages = buildmessage.capture(function () {
names = _.keys(self.overrides);
_.each(self.localPackageDirs, function (dir) {
names = _.union(names, fs.readdirSync(dir));
});
if (self.releaseManifest) {
names = _.union(names, _.keys(self.releaseManifest.packages));
}
_.each(names, function (name) {
var pkg = self.get(name, false);
if (pkg)
ret[name] = pkg;
});
});
if (messages.hasMessages()) {
process.stdout.write("=> Errors while scanning packages:\n\n");
process.stdout.write(messages.formatMessages());
process.exit(1);
}
return ret;
},
// Rebuild all source packages in our search paths -- even including
// any source packages in the warehouse. (Perhaps we shouldn't
// include the warehouse since it's supposed to be immutable.. or
// maybe if the warehouse wants to be immutable perhaps it shouldn't
// include source packages. This is intended primarily for
// convenience when developing the package build code.)
//
// This will force the rebuild even of packages that are
// shadowed. However, for now, it's undefined whether shadowed
// packages are rebuilt (eg, if you have two packages named 'foo' in
// your search path, both of them will have their builds deleted but
// only the visible one might get rebuilt immediately.)
//
// Returns a count of packages rebuilt.
rebuildAll: function () {
var self = this;
// XXX refactor to combine logic with list()? important difference
// here is that we want shadowed packages too
var all = {}; // map from path to name
// Assemble a list of all packages
_.each(self.overrides, function (packageDir, name) {
all[packageDir] = name;
});
_.each(self.localPackageDirs, function (dir) {
var subdirs = fs.readdirSync(dir);
_.each(subdirs, function (subdir) {
var packageDir = path.resolve(dir, subdir);
all[packageDir] = subdir;
});
});
// We *DON'T* look in the warehouse here, because warehouse packages are
// prebuilt.
// Delete any that are source packages with builds.
var count = 0;
_.each(_.keys(all), function (packageDir) {
var isRealPackage = true;
try {
if (! fs.statSync(packageDir).isDirectory())
isRealPackage = false;
} catch (e) {
// stat failed -- path doesn't exist
isRealPackage = false;
}
if (! isRealPackage) {
delete all[packageDir];
return;
}
var buildDir = path.join(packageDir, '.build');
files.rm_recursive(buildDir);
});
// Now reload them, forcing a rebuild. We have to do this in two
// passes because otherwise we might end up rebuilding a package
// and then immediately deleting it.
self.refresh();
_.each(all, function (name, packageDir) {
// Tolerate missing packages. This can happen because our crude
// logic above misdetects an empty directory as a package.
if (self.get(name, /* throwOnError */ false))
count ++;
});
return count;
}
});
var library = exports;
_.extend(exports, {
Library: Library,
// returns a pretty list suitable for showing to the user. input is
// a list of package objects, each of which must have a name (not be
// an application package.)
formatList: function (pkgs) {
var longest = '';
_.each(pkgs, function (pkg) {
if (!pkg.metadata.internal && pkg.name.length > longest.length)
longest = pkg.name;
});
var pad = longest.replace(/./g, ' ');
// it'd be nice to read the actual terminal width, but I tried
// several methods and none of them work (COLUMNS isn't set in
// node's environment; `tput cols` returns a constant 80.) maybe
// node is doing something weird with ptys.
var width = 80;
var out = '';
_.each(pkgs, function (pkg) {
if (pkg.metadata.internal)
return;
var name = pkg.name + pad.substr(pkg.name.length);
var summary = pkg.metadata.summary || 'No description';
out += (name + " " +
summary.substr(0, width - 2 - pad.length) + "\n");
});
return out;
}
});