mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
347 lines
13 KiB
JavaScript
347 lines
13 KiB
JavaScript
var path = require("path");
|
|
var fs = require("fs");
|
|
var os = require("os");
|
|
var Future = require("fibers/future");
|
|
var _ = require("underscore");
|
|
var files = require('./files.js');
|
|
var utils = require('./utils.js');
|
|
var updater = require('./updater.js');
|
|
var httpHelpers = require('./http-helpers.js');
|
|
var fiberHelpers = require('./fiber-helpers.js');
|
|
var release = require('./release.js');
|
|
var archinfo = require('./archinfo.js');
|
|
var catalog = require('./catalog.js');
|
|
var Isopack = require('./isopack.js').Isopack;
|
|
var config = require('./config.js');
|
|
var buildmessage = require('./buildmessage.js');
|
|
|
|
exports.Tropohouse = function (root, catalog) {
|
|
var self = this;
|
|
self.root = root;
|
|
self.catalog = catalog;
|
|
};
|
|
|
|
// Return the directory containing our loaded collection of tools, releases and
|
|
// packages. If we're running an installed version, found at $HOME/.meteor, if
|
|
// we are running form a checkout, probably at $CHECKOUT_DIR/.meteor.
|
|
var defaultWarehouseDir = function () {
|
|
// a hook for tests, or i guess for users.
|
|
if (process.env.METEOR_WAREHOUSE_DIR)
|
|
return process.env.METEOR_WAREHOUSE_DIR;
|
|
|
|
var warehouseBase = files.inCheckout()
|
|
? files.getCurrentToolsDir() : process.env.HOME;
|
|
// XXX This will be `.meteor` soon, once we've written the code to make the
|
|
// tropohouse and warehouse live together in harmony (eg, allowing tropohouse
|
|
// tools to springboard to warehouse tools).
|
|
return path.join(warehouseBase, ".meteor");
|
|
};
|
|
|
|
// The default tropohouse is on disk at defaultWarehouseDir() and knows not to
|
|
// download local packages; you can make your own Tropohouse to override these
|
|
// things.
|
|
exports.default = new exports.Tropohouse(
|
|
defaultWarehouseDir(), catalog.complete);
|
|
|
|
_.extend(exports.Tropohouse.prototype, {
|
|
// Returns the load path where one can expect to find the package, at a given
|
|
// version, if we have already downloaded from the package server. Does not
|
|
// check for contents.
|
|
//
|
|
// Returns null if the package name is lexographically invalid.
|
|
packagePath: function (packageName, version, relative) {
|
|
var self = this;
|
|
if (! utils.isValidPackageName(packageName)) {
|
|
return null;
|
|
}
|
|
|
|
var relativePath = path.join(config.getPackagesDirectoryName(),
|
|
packageName, version);
|
|
return relative ? relativePath : path.join(self.root, relativePath);
|
|
},
|
|
|
|
// Pretty extreme! We call this when we learn that something has changed on
|
|
// the server in a way that our sync protocol doesn't understand well.
|
|
wipeAllPackages: function () {
|
|
var self = this;
|
|
|
|
var packagesDirectoryName = config.getPackagesDirectoryName();
|
|
|
|
var packageRootDir = path.join(self.root, packagesDirectoryName);
|
|
try {
|
|
var packages = fs.readdirSync(packageRootDir);
|
|
} catch (e) {
|
|
// No packages at all? We're done.
|
|
if (e.code === 'ENOENT')
|
|
return;
|
|
throw e;
|
|
}
|
|
|
|
// We want to be careful not to break the 'meteor' symlink inside the
|
|
// tropohouse. Hopefully nobody deleted/modified that package!
|
|
var latestToolPackage = null;
|
|
var latestToolVersion = null;
|
|
var currentToolPackage = null;
|
|
var currentToolVersion = null;
|
|
// Warning: we can't examine release.current here, because we might be
|
|
// currently processing release.load!
|
|
if (!files.inCheckout()) {
|
|
// toolsDir is something like:
|
|
// /home/user/.meteor/packages/meteor-tool/.1.0.17.ut200e++os.osx.x86_64+web.browser+web.cordova/meteor-tool-os.osx.x86_64
|
|
var toolsDir = files.getCurrentToolsDir();
|
|
// eg, 'meteor-tool'
|
|
currentToolPackage = path.basename(path.dirname(path.dirname(toolsDir)));
|
|
// eg, '.1.0.17-xyz1.2.ut200e++os.osx.x86_64+web.browser+web.cordova'
|
|
var toolVersionDir = path.basename(path.dirname(toolsDir));
|
|
var toolVersionWithDotAndRandomBit = toolVersionDir.split('++')[0];
|
|
var pieces = toolVersionWithDotAndRandomBit.split('.');
|
|
pieces.shift();
|
|
pieces.pop();
|
|
currentToolVersion = pieces.join('.');
|
|
var latestMeteorSymlink = self.latestMeteorSymlink();
|
|
if (utils.startsWith(latestMeteorSymlink,
|
|
packagesDirectoryName + path.sep)) {
|
|
var rest = latestMeteorSymlink.substr(packagesDirectoryName.length + path.sep.length);
|
|
var pieces = rest.split(path.sep);
|
|
latestToolPackage = pieces[0];
|
|
latestToolVersion = pieces[1];
|
|
}
|
|
}
|
|
|
|
_.each(packages, function (package) {
|
|
var packageDir = path.join(packageRootDir, package);
|
|
try {
|
|
var versions = fs.readdirSync(packageDir);
|
|
} catch (e) {
|
|
// Somebody put a file in here or something? Whatever, ignore.
|
|
if (e.code === 'ENOENT' || e.code === 'ENOTDIR')
|
|
return;
|
|
throw e;
|
|
}
|
|
_.each(fs.readdirSync(packageDir), function (version) {
|
|
// Is this a pre-0.9.0 "warehouse" version with a hash name?
|
|
if (/^[a-f0-9]{3,}$/.test(version))
|
|
return;
|
|
|
|
// Skip the currently-latest tool (ie, don't break top-level meteor
|
|
// symlink). This includes both the symlink with its name and the thing
|
|
// it points to.
|
|
if (package === latestToolPackage &&
|
|
(version === latestToolVersion ||
|
|
utils.startsWith(version, '.' + latestToolVersion + '.'))) {
|
|
return;
|
|
}
|
|
|
|
// Skip the currently-executing tool (ie, don't break the current
|
|
// operation).
|
|
if (package === currentToolPackage &&
|
|
(version === currentToolVersion ||
|
|
utils.startsWith(version, '.' + currentToolVersion + '.'))) {
|
|
return;
|
|
}
|
|
|
|
files.rm_recursive(path.join(packageDir, version));
|
|
});
|
|
});
|
|
},
|
|
|
|
// Contacts the package server, downloads and extracts a tarball for a given
|
|
// buildRecord into a temporary directory, whose path is returned.
|
|
//
|
|
// XXX: Error handling.
|
|
downloadBuildToTempDir: function (versionInfo, buildRecord) {
|
|
var self = this;
|
|
var targetDirectory = files.mkdtemp();
|
|
|
|
var url = buildRecord.build.url;
|
|
|
|
var progress = buildmessage.addChildTracker("Downloading build");
|
|
try {
|
|
buildmessage.capture({}, function () {
|
|
var packageTarball = httpHelpers.getUrl({
|
|
url: url,
|
|
encoding: null,
|
|
progress: progress,
|
|
wait: false
|
|
});
|
|
files.extractTarGz(packageTarball, targetDirectory);
|
|
});
|
|
} finally {
|
|
progress.reportProgressDone();
|
|
}
|
|
|
|
return targetDirectory;
|
|
},
|
|
|
|
// Given versionInfo for a package version and required architectures, checks
|
|
// to make sure that we have the package at the requested arch. If we do not
|
|
// have the package, contact the server and attempt to download and extract
|
|
// the right build.
|
|
//
|
|
// XXX more precise error handling in offline case. maybe throw instead like
|
|
// warehouse does. actually, generally deal with error handling.
|
|
maybeDownloadPackageForArchitectures: function (options) {
|
|
var self = this;
|
|
buildmessage.assertInCapture();
|
|
if (!options.packageName)
|
|
throw Error("Missing required argument: packageName");
|
|
if (!options.version)
|
|
throw Error("Missing required argument: version");
|
|
if (!options.architectures)
|
|
throw Error("Missing required argument: architectures");
|
|
|
|
var packageName = options.packageName;
|
|
var version = options.version;
|
|
|
|
// If this package isn't coming from the package server (loaded from a
|
|
// checkout, or from an app package directory), don't try to download it (we
|
|
// already have it)
|
|
// (In the special case of springboarding, we avoid using self.catalog
|
|
// here because it is catalog.complete and is not yet initialized.)
|
|
if (!options.definitelyNotLocal && self.catalog.isLocalPackage(packageName))
|
|
return;
|
|
|
|
// Figure out what arches (if any) we have loaded for this package version
|
|
// already.
|
|
var packageLinkFile = self.packagePath(packageName, version);
|
|
var downloadedArches = [];
|
|
var packageLinkTarget = null;
|
|
try {
|
|
packageLinkTarget = fs.readlinkSync(packageLinkFile);
|
|
} catch (e) {
|
|
// Complain about anything other than "we don't have it at all". This
|
|
// includes "not a symlink": The main reason this would not be a symlink
|
|
// is if it's a directory containing a pre-0.9.0 package (ie, this is a
|
|
// warehouse package not a tropohouse package). But the versions should
|
|
// not overlap: warehouse versions are truncated SHAs whereas tropohouse
|
|
// versions should be semver-like.
|
|
if (e.code !== 'ENOENT')
|
|
throw e;
|
|
}
|
|
if (packageLinkTarget) {
|
|
// The symlink will be of the form '.VERSION.RANDOMTOKEN++web.browser+os',
|
|
// so this strips off the part before the '++'.
|
|
// XXX maybe we should just read the isopack.json instead of
|
|
// depending on the symlink?
|
|
var archPart = packageLinkTarget.split('++')[1];
|
|
if (!archPart)
|
|
throw Error("unexpected symlink target for " + packageName + "@" +
|
|
version + ": " + packageLinkTarget);
|
|
downloadedArches = archPart.split('+');
|
|
}
|
|
|
|
var archesToDownload = _.filter(options.architectures, function (requiredArch) {
|
|
return !archinfo.mostSpecificMatch(requiredArch, downloadedArches);
|
|
});
|
|
|
|
// Have everything we need? Great.
|
|
if (!archesToDownload.length) {
|
|
return;
|
|
}
|
|
|
|
// Since we are downloading from the server (and we've already done the
|
|
// local package check), we can use the official catalog here. (This is
|
|
// important, since springboarding calls this function before the complete
|
|
// catalog is ready!)
|
|
var buildsToDownload = catalog.official.getBuildsForArches(
|
|
packageName, version, archesToDownload);
|
|
if (! buildsToDownload) {
|
|
var e = new Error(
|
|
"No compatible build found for " + packageName + "@" + version);
|
|
e.noCompatibleBuildError = true;
|
|
throw e;
|
|
}
|
|
|
|
buildmessage.enterJob({
|
|
title: " Installing " + packageName + "@" + version + "..."
|
|
}, function() {
|
|
var buildTempDirs = [];
|
|
// If there's already a package in the tropohouse, start with it.
|
|
if (packageLinkTarget) {
|
|
buildTempDirs.push(path.resolve(path.dirname(packageLinkFile),
|
|
packageLinkTarget));
|
|
}
|
|
// XXX how does concurrency work here? we could just get errors if we try
|
|
// to rename over the other thing? but that's the same as in warehouse?
|
|
_.each(buildsToDownload, function (build) {
|
|
buildTempDirs.push(self.downloadBuildToTempDir({packageName: packageName, version: version}, build));
|
|
});
|
|
|
|
// We need to turn our builds into a single isopack.
|
|
var isopack = new Isopack;
|
|
_.each(buildTempDirs, function (buildTempDir, i) {
|
|
isopack._loadUnibuildsFromPath(
|
|
packageName,
|
|
buildTempDir,
|
|
{firstIsopack: i === 0});
|
|
});
|
|
// Note: wipeAllPackages depends on this filename structure, as does the
|
|
// part above which readlinks.
|
|
var newPackageLinkTarget = '.' + version + '.'
|
|
+ utils.randomToken() + '++' + isopack.buildArchitectures();
|
|
var combinedDirectory = self.packagePath(packageName, newPackageLinkTarget);
|
|
isopack.saveToPath(combinedDirectory, {
|
|
// We got this from the server, so we can't rebuild it.
|
|
elideBuildInfo: true
|
|
});
|
|
files.symlinkOverSync(newPackageLinkTarget, packageLinkFile);
|
|
|
|
// Clean up old version.
|
|
if (packageLinkTarget) {
|
|
files.rm_recursive(self.packagePath(packageName, packageLinkTarget));
|
|
}
|
|
});
|
|
|
|
return;
|
|
},
|
|
|
|
|
|
// Go through a list of packages and makes sure we have enough builds of the
|
|
// package downloaded such that we can load a browser unibuild and a unibuild
|
|
// that will run on this system (or the requested architecture). Return the
|
|
// object with mapping packageName to version for the packages that we have
|
|
// successfully downloaded.
|
|
//
|
|
// XXX This function's error handling capabilities are poor. It's supposed to
|
|
// return a data structure that its callers check, but most of its callers
|
|
// don't check it. Bleah. Should rewrite this and all of its callers.
|
|
downloadMissingPackages: function (versionMap, options) {
|
|
var self = this;
|
|
buildmessage.assertInCapture();
|
|
options = options || {};
|
|
var serverArch = options.serverArch || archinfo.host();
|
|
var downloadedPackages = {};
|
|
buildmessage.forkJoin({ title: 'Downloading packages', parallel: true },
|
|
versionMap, function (version, name) {
|
|
try {
|
|
self.maybeDownloadPackageForArchitectures({
|
|
packageName: name,
|
|
version: version,
|
|
architectures: [serverArch]
|
|
});
|
|
downloadedPackages[name] = version;
|
|
} catch (err) {
|
|
if (!(err.noCompatibleBuildError))
|
|
throw err;
|
|
console.log(err.message);
|
|
// continue, which is weird, but we want to avoid a stack trace...
|
|
// the caller is supposed to check the size of the return value,
|
|
// although many callers do not.
|
|
}
|
|
});
|
|
return downloadedPackages;
|
|
},
|
|
|
|
latestMeteorSymlink: function () {
|
|
var self = this;
|
|
var linkPath = path.join(self.root, 'meteor');
|
|
return fs.readlinkSync(linkPath);
|
|
},
|
|
|
|
replaceLatestMeteorSymlink: function (linkText) {
|
|
var self = this;
|
|
var linkPath = path.join(self.root, 'meteor');
|
|
files.symlinkOverSync(linkText, linkPath);
|
|
}
|
|
});
|