mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
471 lines
17 KiB
JavaScript
471 lines
17 KiB
JavaScript
/// We store a "warehouse" of tools, releases and packages on
|
|
/// disk. This warehouse is populated from our servers, as needed.
|
|
///
|
|
/// Directory structure:
|
|
///
|
|
/// meteor (relative path symlink to tools/latest/bin/meteor)
|
|
/// tools/ (not in checkout, since we run against checked-out code)
|
|
/// latest/ (relative path symlink to latest VERSION/ tools directory)
|
|
/// VERSION/
|
|
/// releases/
|
|
/// latest (relative path symlink to latest x.y.z.release.json)
|
|
/// x.y.z.release.json
|
|
/// x.y.z.notices.json
|
|
/// packages/
|
|
/// foo/
|
|
/// VERSION/
|
|
///
|
|
/// When running from a checkout, there is only one acceptable release - 'none',
|
|
/// which has an empty manifest, ensuring that we only load local packages (in
|
|
/// CHECKOUT/packages or within a directory in the PACKAGE_DIRS environment
|
|
/// variable)
|
|
|
|
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 updater = require('./updater.js');
|
|
var fiberHelpers = require('./fiber-helpers.js');
|
|
var logging = require('./logging.js');
|
|
|
|
var WAREHOUSE_URLBASE = 'https://warehouse.meteor.com';
|
|
|
|
// Like fs.symlinkSync, but creates a temporay link and renames it over the
|
|
// file; this means it works even if the file already exists.
|
|
var symlinkOverSync = function (linkText, file) {
|
|
var tmpSymlink = file + ".tmp" + files._randomToken();
|
|
fs.symlinkSync(linkText, tmpSymlink);
|
|
fs.renameSync(tmpSymlink, file);
|
|
};
|
|
|
|
var warehouse = exports;
|
|
_.extend(warehouse, {
|
|
// Return our loaded collection of tools, releases and
|
|
// packages. If we're running an installed version, found at
|
|
// $HOME/.meteor.
|
|
getWarehouseDir: function () {
|
|
// a hook for tests, or i guess for users.
|
|
if (process.env.METEOR_WAREHOUSE_DIR)
|
|
return process.env.METEOR_WAREHOUSE_DIR;
|
|
|
|
// This function should never be called unless we have a warehouse
|
|
// (an installed version, or with process.env.METEOR_WAREHOUSE_DIR
|
|
// set)
|
|
if (!files.usesWarehouse())
|
|
throw new Error("There's no warehouse in a git checkout");
|
|
|
|
return path.join(process.env.HOME, '.meteor');
|
|
},
|
|
|
|
getToolsDir: function (version) {
|
|
return path.join(warehouse.getWarehouseDir(), 'tools', version);
|
|
},
|
|
|
|
getToolsFreshFile: function (version) {
|
|
return path.join(warehouse.getWarehouseDir(), 'tools', version, '.fresh');
|
|
},
|
|
|
|
// If you're running from a git checkout, only accept 'none' and
|
|
// return an empty manifest. Otherwise, ensure the passed release
|
|
// version is stored in the local warehouse and return its parsed
|
|
// manifest.
|
|
ensureReleaseExistsAndReturnManifest: function(release) {
|
|
if (release === 'none')
|
|
return null;
|
|
if (!files.usesWarehouse())
|
|
throw new Error("Not in a warehouse but requesting a manifest!");
|
|
|
|
var manifestPath = path.join(
|
|
warehouse.getWarehouseDir(), 'releases', release + '.release.json');
|
|
|
|
return warehouse._populateWarehouseForRelease(release);
|
|
},
|
|
|
|
_latestReleaseSymlinkPath: function () {
|
|
return path.join(warehouse.getWarehouseDir(), 'releases', 'latest');
|
|
},
|
|
|
|
// look in the warehouse for the latest release version. if no
|
|
// releases are found, return null.
|
|
latestRelease: function() {
|
|
var latestReleaseSymlink = warehouse._latestReleaseSymlinkPath();
|
|
// This throws if the symlink doesn't exist, but it really should, since
|
|
// it exists in bootstrap tarballs and is never deleted.
|
|
var linkText = fs.readlinkSync(latestReleaseSymlink);
|
|
return linkText.replace(/\.release\.json$/, '');
|
|
},
|
|
|
|
_latestToolsSymlinkPath: function () {
|
|
return path.join(warehouse.getWarehouseDir(), 'tools', 'latest');
|
|
},
|
|
|
|
// Look in the warehouse for the latest tools version. (This is the one that
|
|
// the meteor shell script runs initially). If the symlink doesn't exist
|
|
// (which shouldn't happen, since it is provided in the bootstrap tarball)
|
|
// returns null.
|
|
latestTools: function() {
|
|
var latestToolsSymlink = warehouse._latestToolsSymlinkPath();
|
|
try {
|
|
return fs.readlinkSync(latestToolsSymlink);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// returns true if we updated the latest symlink
|
|
// XXX make errors prettier
|
|
fetchLatestRelease: function (background) {
|
|
var manifest = updater.getManifest();
|
|
|
|
// XXX in the future support release channels other than stable
|
|
var releaseName = manifest && manifest.releases &&
|
|
manifest.releases.stable && manifest.releases.stable.version;
|
|
if (!releaseName) {
|
|
if (background)
|
|
return false; // it's in the background, who cares.
|
|
logging.die("No stable release found.");
|
|
}
|
|
|
|
var latestReleaseManifest =
|
|
warehouse._populateWarehouseForRelease(releaseName, background);
|
|
|
|
// First, make sure the latest tools symlink reflects the latest installed
|
|
// release.
|
|
if (latestReleaseManifest.tools !== warehouse.latestTools()) {
|
|
symlinkOverSync(latestReleaseManifest.tools,
|
|
warehouse._latestToolsSymlinkPath());
|
|
}
|
|
|
|
var storedLatestRelease = warehouse.latestRelease();
|
|
if (storedLatestRelease === releaseName)
|
|
return false;
|
|
|
|
symlinkOverSync(releaseName + '.release.json',
|
|
warehouse._latestReleaseSymlinkPath());
|
|
return true;
|
|
},
|
|
|
|
packageExistsInWarehouse: function (name, version) {
|
|
// Look for presence of "package.js" file in directory so we don't count
|
|
// an empty dir as a package. An empty dir could be left by a failed
|
|
// package untarring, for example.
|
|
return fs.existsSync(
|
|
path.join(warehouse.getWarehouseDir(), 'packages', name, version, 'package.js'));
|
|
},
|
|
|
|
getPackageFreshFile: function (name, version) {
|
|
return path.join(warehouse.getWarehouseDir(), 'packages', name, version, '.fresh');
|
|
},
|
|
|
|
toolsExistsInWarehouse: function (version) {
|
|
return fs.existsSync(warehouse.getToolsDir(version));
|
|
},
|
|
|
|
_calculateNewPiecesForRelease: function (releaseManifest) {
|
|
// newPieces.tools and newPieces.packages[PACKAGE] are either falsey (if
|
|
// nothing is new), or an object with keys "version" and bool
|
|
// "needsDownload". "needsDownload" is true if the piece is not in the
|
|
// warehouse, and is false if it's in the warehouse but has never been used.
|
|
var newPieces = {
|
|
tools: null,
|
|
packages: {}
|
|
};
|
|
|
|
// populate warehouse with tools version for this release
|
|
var toolsVersion = releaseManifest.tools;
|
|
if (!warehouse.toolsExistsInWarehouse(toolsVersion)) {
|
|
newPieces.tools = {version: toolsVersion, needsDownload: true};
|
|
} else if (fs.existsSync(warehouse.getToolsFreshFile(toolsVersion))) {
|
|
newPieces.tools = {version: toolsVersion, needsDownload: false};
|
|
}
|
|
|
|
_.each(releaseManifest.packages, function (version, name) {
|
|
if (!warehouse.packageExistsInWarehouse(name, version)) {
|
|
newPieces.packages[name] = {version: version, needsDownload: true};
|
|
} else if (fs.existsSync(warehouse.getPackageFreshFile(name, version))) {
|
|
newPieces.packages[name] = {version: version, needsDownload: false};
|
|
}
|
|
});
|
|
if (newPieces.tools || !_.isEmpty(newPieces.packages))
|
|
return newPieces;
|
|
return null;
|
|
},
|
|
|
|
_packageUpdatesMessage: function (packageNames) {
|
|
var lines = [];
|
|
var width = 80; // see library.formatList for why we hardcode this
|
|
var currentLine = ' * Package updates:';
|
|
_.each(packageNames, function (name) {
|
|
if (currentLine.length + 1 + name.length <= width) {
|
|
currentLine += ' ' + name;
|
|
} else {
|
|
lines.push(currentLine);
|
|
currentLine = ' ' + name;
|
|
}
|
|
});
|
|
lines.push(currentLine);
|
|
return lines.join('\n');
|
|
},
|
|
|
|
// fetches the manifest file for the given release version. also fetches
|
|
// all of the missing versioned packages referenced from the release manifest
|
|
// @param releaseVersion {String} eg "0.1"
|
|
_populateWarehouseForRelease: function(releaseVersion, background) {
|
|
var future = new Future;
|
|
var releasesDir = path.join(warehouse.getWarehouseDir(), 'releases');
|
|
files.mkdir_p(releasesDir, 0755);
|
|
var releaseManifestPath = path.join(releasesDir,
|
|
releaseVersion + '.release.json');
|
|
|
|
// If the release already exists, we don't have to do anything, except maybe
|
|
// print a message if this release has never been used before (and we only
|
|
// have it due to a background download).
|
|
var releaseAlreadyExists = true;
|
|
try {
|
|
var releaseManifestText = fs.readFileSync(releaseManifestPath);
|
|
} catch (e) {
|
|
releaseAlreadyExists = false;
|
|
}
|
|
|
|
// Now get release manifest if we don't already have it, but only write it
|
|
// after we're done writing packages
|
|
if (!releaseAlreadyExists) {
|
|
try {
|
|
releaseManifestText = files.getUrl(
|
|
WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".release.json");
|
|
} catch (e) {
|
|
// just throw, if we're in the background anyway, or if this is the
|
|
// OfflineError which should be handled by the caller
|
|
if (background || e instanceof files.OfflineError)
|
|
throw e;
|
|
// We actually got some response, so we're probably online and we
|
|
// just can't find the release.
|
|
logging.die(releaseVersion + ": unknown release.");
|
|
}
|
|
}
|
|
|
|
var releaseManifest = JSON.parse(releaseManifestText);
|
|
|
|
var newPieces = warehouse._calculateNewPiecesForRelease(releaseManifest);
|
|
|
|
if (releaseAlreadyExists && !newPieces)
|
|
return releaseManifest;
|
|
|
|
if (newPieces && !background) {
|
|
console.log("Installing Meteor %s:", releaseVersion);
|
|
if (newPieces.tools) {
|
|
console.log(" * 'meteor' build tool (version %s)",
|
|
newPieces.tools.version);
|
|
}
|
|
if (!_.isEmpty(newPieces.packages)) {
|
|
console.log(warehouse._packageUpdatesMessage(
|
|
_.keys(newPieces.packages).sort()));
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
if (!releaseAlreadyExists) {
|
|
if (newPieces && newPieces.tools && newPieces.tools.needsDownload) {
|
|
try {
|
|
warehouse.downloadToolsToWarehouse(
|
|
newPieces.tools.version,
|
|
warehouse._platform(),
|
|
warehouse.getWarehouseDir());
|
|
} catch (e) {
|
|
if (!background)
|
|
console.error("Failed to load tools for release " + releaseVersion);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
var packagesToDownload = {};
|
|
_.each(newPieces && newPieces.packages, function (packageInfo, name) {
|
|
if (packageInfo.needsDownload)
|
|
packagesToDownload[name] = packageInfo.version;
|
|
});
|
|
if (!_.isEmpty(packagesToDownload)) {
|
|
try {
|
|
warehouse.downloadPackagesToWarehouse(packagesToDownload,
|
|
warehouse._platform(),
|
|
warehouse.getWarehouseDir());
|
|
} catch (e) {
|
|
if (!background)
|
|
console.error("Failed to load packages for release " +
|
|
releaseVersion);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// try getting the releases's notices. only blessed releases have one, so
|
|
// if we can't find it just proceed.
|
|
try {
|
|
var notices = files.getUrl(
|
|
WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".notices.json");
|
|
|
|
// Real notices are valid JSON.
|
|
JSON.parse(notices);
|
|
|
|
fs.writeFileSync(
|
|
path.join(releasesDir, releaseVersion + '.notices.json'), notices);
|
|
} catch (e) {
|
|
// no notices, proceed
|
|
}
|
|
|
|
// Now that we have written all packages, it's safe to write the
|
|
// release manifest.
|
|
fs.writeFileSync(releaseManifestPath, releaseManifestText);
|
|
}
|
|
|
|
// Finally, clear the "fresh" files for all the things we just printed
|
|
// (whether or not we just downloaded them), unless we were in the
|
|
// background and printed nothing.
|
|
if (newPieces && !background) {
|
|
if (newPieces.tools) {
|
|
fs.unlinkSync(warehouse.getToolsFreshFile(newPieces.tools.version));
|
|
}
|
|
_.each(newPieces.packages, function (packageInfo, name) {
|
|
fs.unlinkSync(warehouse.getPackageFreshFile(name, packageInfo.version));
|
|
});
|
|
}
|
|
|
|
return releaseManifest;
|
|
},
|
|
|
|
// this function is also used by bless-release.js
|
|
downloadToolsToWarehouse: function (
|
|
toolsVersion, platform, warehouseDirectory, dontWriteFreshFile) {
|
|
// XXX this sucks. We store all the tarballs in memory. This is huge.
|
|
// We should instead stream packages in parallel. Since the node stream
|
|
// API is in flux, we should probably wait a bit.
|
|
// http://blog.nodejs.org/2012/12/20/streams2/
|
|
|
|
var toolsTarballFilename =
|
|
"meteor-tools-" + toolsVersion + "-" + platform + ".tar.gz";
|
|
var toolsTarballPath = "/tools/" + toolsVersion + "/"
|
|
+ toolsTarballFilename;
|
|
var toolsTarball = files.getUrl({
|
|
url: WAREHOUSE_URLBASE + toolsTarballPath,
|
|
encoding: null
|
|
});
|
|
files.extractTarGz(toolsTarball,
|
|
path.join(warehouseDirectory, 'tools', toolsVersion));
|
|
if (!dontWriteFreshFile)
|
|
fs.writeFileSync(warehouse.getToolsFreshFile(toolsVersion), '');
|
|
},
|
|
|
|
printNotices: function(fromRelease, toRelease, packages) {
|
|
var noticesPath = path.join(
|
|
warehouse.getWarehouseDir(), 'releases', toRelease + '.notices.json');
|
|
|
|
try {
|
|
var notices = JSON.parse(fs.readFileSync(noticesPath));
|
|
} catch (e) {
|
|
// It's valid for this file to not exist (if it's an unblessed version)
|
|
// and eh, if the JSON is bad then the user doesn't really care.
|
|
return;
|
|
}
|
|
|
|
var noticesToPrint = [];
|
|
// If we are updating from an app with no .meteor/release, print all
|
|
// entries up to toRelease.
|
|
var foundFromRelease = !fromRelease;
|
|
for (var i = 0; i < notices.length; ++i) {
|
|
var record = notices[i];
|
|
// We want to print the notices for releases newer than fromRelease, and
|
|
// we always want to print toRelease even if we're updating from something
|
|
// that's not in the notices file at all.
|
|
if (foundFromRelease || record.release === toRelease) {
|
|
var noticesForRelease = record.notices || [];
|
|
_.each(record.packageNotices, function (lines, pkgName) {
|
|
if (_.contains(packages, pkgName)) {
|
|
if (!_.isEmpty(noticesForRelease))
|
|
noticesForRelease.push('');
|
|
noticesForRelease.push.apply(noticesForRelease, lines);
|
|
}
|
|
});
|
|
|
|
if (!_.isEmpty(noticesForRelease)) {
|
|
noticesToPrint.push({release: record.release,
|
|
notices: noticesForRelease});
|
|
}
|
|
}
|
|
// Nothing newer than toRelease.
|
|
if (record.release === toRelease)
|
|
break;
|
|
if (!foundFromRelease && record.release === fromRelease)
|
|
foundFromRelease = true;
|
|
}
|
|
|
|
if (_.isEmpty(noticesToPrint))
|
|
return;
|
|
|
|
console.log();
|
|
console.log("-- Notice --");
|
|
console.log();
|
|
_.each(noticesToPrint, function (record) {
|
|
var header = record.release + ': ';
|
|
_.each(record.notices, function (line, i) {
|
|
console.log(header + line);
|
|
if (i === 0)
|
|
header = header.replace(/./g, ' ');
|
|
});
|
|
console.log();
|
|
});
|
|
},
|
|
|
|
// this function is also used by bless-release.js
|
|
downloadPackagesToWarehouse: function (packagesToDownload,
|
|
platform,
|
|
warehouseDirectory,
|
|
dontWriteFreshFile) {
|
|
fiberHelpers.parallelEach(
|
|
packagesToDownload, function (version, name) {
|
|
var packageDir = path.join(
|
|
warehouseDirectory, 'packages', name, version);
|
|
var packageUrl = WAREHOUSE_URLBASE + "/packages/" + name +
|
|
"/" + version +
|
|
"/" + name + '-' + version + "-" + platform + ".tar.gz";
|
|
|
|
var tarball = files.getUrl({url: packageUrl, encoding: null});
|
|
files.extractTarGz(tarball, packageDir);
|
|
if (!dontWriteFreshFile)
|
|
fs.writeFileSync(warehouse.getPackageFreshFile(name, version), '');
|
|
});
|
|
},
|
|
|
|
_lastPrintedBannerReleaseFile: function () {
|
|
return path.join(warehouse.getWarehouseDir(),
|
|
'releases', '.last-printed-banner');
|
|
},
|
|
|
|
lastPrintedBannerRelease: function () {
|
|
// Calculate filename outside of try block, because getWarehouseDir can
|
|
// throw.
|
|
var filename = warehouse._lastPrintedBannerReleaseFile();
|
|
try {
|
|
return fs.readFileSync(filename, 'utf8');
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
writeLastPrintedBannerRelease: function (release) {
|
|
fs.writeFileSync(warehouse._lastPrintedBannerReleaseFile(), release);
|
|
},
|
|
|
|
_platform: function () {
|
|
// Normalize from Node "os.arch()" to "uname -m".
|
|
var arch = os.arch();
|
|
if (arch === "ia32")
|
|
arch = "i686";
|
|
else if (arch === "x64")
|
|
arch = "x86_64";
|
|
else
|
|
throw new Error("Unsupported architecture " + arch);
|
|
return os.type() + "_" + arch;
|
|
}
|
|
});
|