text: var assert = require("assert");
  var _ = require('underscore');
  
  var archinfo = require('./utils/archinfo.js');
  var buildmessage = require('./utils/buildmessage.js');
  var catalog = require('./packaging/catalog/catalog.js');
  var catalogLocal = require('./packaging/catalog/catalog-local.js');
  var Console = require('./console/console.js').Console;
  var files = require('./fs/files.js');
  var isopackCacheModule = require('./isobuild/isopack-cache.js');
  var isopackets = require('./tool-env/isopackets.js');
  var packageMapModule = require('./packaging/package-map.js');
  var release = require('./packaging/release.js');
  var tropohouse = require('./packaging/tropohouse.js');
  var utils = require('./utils/utils.js');
  var watch = require('./fs/watch.js');
  var Profile = require('./tool-env/profile.js').Profile;
  
  // The ProjectContext represents all the context associated with an app:
  // metadata files in the `.meteor` directory, the choice of package versions
  // used by it, etc.  Any time you want to work with an app, create a
  // ProjectContext and call prepareProjectForBuild on it (in a buildmessage
  // context).
  //
  // Note that this should only be used by parts of the code that truly require a
  // full project to exist; you won't find any reference to ProjectContext in
  // compiler.js or isopack.js, which work on individual files (though they will
  // get references to some of the objects which can be stored in a ProjectContext
  // such as PackageMap and IsopackCache).  Parts of the code that should deal
  // with ProjectContext include command implementations, the parts of bundler.js
  // that deal with creating a full project, PackageSource.initFromAppDir, stats
  // reporting, etc.
  //
  // Classes in this file follow the standard protocol where names beginning with
  // _ should not be externally accessed.
  function ProjectContext(options) {
    var self = this;
    assert.ok(self instanceof ProjectContext);
  
    if (!options.projectDir)
      throw Error("missing projectDir!");
  
    self.originalOptions = options;
    self.reset();
  }
  exports.ProjectContext = ProjectContext;
  
  // The value is the name of the method to call to continue.
  var STAGE = {
    INITIAL: '_readProjectMetadata',
    READ_PROJECT_METADATA: '_initializeCatalog',
    INITIALIZE_CATALOG: '_resolveConstraints',
    RESOLVE_CONSTRAINTS: '_downloadMissingPackages',
    DOWNLOAD_MISSING_PACKAGES: '_buildLocalPackages',
    BUILD_LOCAL_PACKAGES: '_saveChangedMetadata',
    SAVE_CHANGED_METADATA: 'DONE'
  };
  
  _.extend(ProjectContext.prototype, {
    reset: function (moreOptions, resetOptions) {
      var self = this;
      // Allow overriding some options until the next call to reset; used by
      // 'meteor update' code to try various values of releaseForConstraints.
      var options = _.extend({}, self.originalOptions, moreOptions);
      // This is options that are actually directed at reset itself.
      resetOptions = resetOptions || {};
  
      self.projectDir = options.projectDir;
      self.tropohouse = options.tropohouse || tropohouse.default;
  
      self._packageMapFilename = options.packageMapFilename ||
        files.pathJoin(self.projectDir, '.meteor', 'versions');
  
      self._serverArchitectures = options.serverArchitectures || [];
      // We always need to download host versions of packages, at least for
      // plugins.
      self._serverArchitectures.push(archinfo.host());
      self._serverArchitectures = _.uniq(self._serverArchitectures);
  
      // test-packages overrides this to load local packages from your real app
      // instead of from test-runner-app.
      self._projectDirForLocalPackages = options.projectDirForLocalPackages ||
        options.projectDir;
      self._explicitlyAddedLocalPackageDirs =
        options.explicitlyAddedLocalPackageDirs;
  
      // Used by 'meteor rebuild'; true to rebuild all packages, or a list of
      // package names.  Deletes the isopacks and their plugin caches.
      self._forceRebuildPackages = options.forceRebuildPackages;
  
      // Set in a few cases where we really want to only get packages from
      // checkout.
      self._ignorePackageDirsEnvVar = options.ignorePackageDirsEnvVar;
  
      // Set by some tests where we want to pretend that we don't have packages in
      // the git checkout (because they're using a fake warehouse).
      self._ignoreCheckoutPackages = options.ignoreCheckoutPackages;
  
      // Set by some tests to override the official catalog.
      self._officialCatalog = options.officialCatalog || catalog.official;
  
      if (options.alwaysWritePackageMap && options.neverWritePackageMap)
        throw Error("always or never?");
  
      // Set by 'meteor create' and 'meteor update' to ensure that
      // .meteor/versions is always written even if release.current does not match
      // the project's release.
      self._alwaysWritePackageMap = options.alwaysWritePackageMap;
  
      // Set by a few special-case commands that call
      // projectConstraintsFile.addConstraints for internal reasons without
      // intending to actually write .meteor/packages and .meteor/versions (eg,
      // 'publish' wants to make sure making sure the test is built, and
      // --get-ready wants to build every conceivable package).
      self._neverWriteProjectConstraintsFile =
        options.neverWriteProjectConstraintsFile;
      self._neverWritePackageMap = options.neverWritePackageMap;
  
      // Set by 'meteor update' to specify which packages may be updated. Array of
      // package names.
      self._upgradePackageNames = options.upgradePackageNames;
      // Set by 'meteor update' to mean that we should upgrade the
      // "patch" (and wrapNum, etc.) parts of indirect dependencies.
      self._upgradeIndirectDepPatchVersions =
        options.upgradeIndirectDepPatchVersions;
  
      // Set by publishing commands to ensure that published packages always have
      // a web.cordova slice (because we aren't yet smart enough to just default
      // to using the web.browser slice instead or make a common 'web' slice).
      self._forceIncludeCordovaUnibuild = options.forceIncludeCordovaUnibuild;
  
      // If explicitly specified as null, use no release for constraints.
      // If specified non-null, should be a release version catalog record.
      // If not specified, defaults to release.current.
      //
      // Note that NONE of these cases are "use the release from
      // self.releaseFile"; after all, if you are explicitly running `meteor
      // --release foo` it will override what is found in .meteor/releases.
      if (_.has(options, 'releaseForConstraints')) {
        self._releaseForConstraints = options.releaseForConstraints || null;
      } else if (release.current.isCheckout()) {
        self._releaseForConstraints = null;
      } else {
        self._releaseForConstraints = release.current.getCatalogReleaseData();
      }
  
      if (resetOptions.preservePackageMap && self.packageMap) {
        self._cachedVersionsBeforeReset = self.packageMap.toVersionMap();
        // packageMapFile should always exist if packageMap does
        self._oldPackageMapFileHash = self.packageMapFile.fileHash;
      } else {
        self._cachedVersionsBeforeReset = null;
        self._oldPackageMapFileHash = null;
      }
  
      // The --allow-incompatible-update command-line switch, which allows
      // the version solver to choose versions of root dependencies that are
      // incompatible with the previously chosen versions (i.e. to downgrade
      // them or change their major version).
      self._allowIncompatibleUpdate = options.allowIncompatibleUpdate;
  
      // If set, we run the linter on the app and local packages.  Set by 'meteor
      // lint', and the runner commands (run/test-packages/debug) when --no-lint
      // is not passed.
      self.lintAppAndLocalPackages = options.lintAppAndLocalPackages;
  
      // If set, we run the linter on just one local package, with this
      // source root. Set by 'meteor lint' in a package, and 'meteor publish'.
      self._lintPackageWithSourceRoot = options.lintPackageWithSourceRoot;
  
      // Initialized by readProjectMetadata.
      self.releaseFile = null;
      self.projectConstraintsFile = null;
      self.packageMapFile = null;
      self.platformList = null;
      self.cordovaPluginsFile = null;
      self.appIdentifier = null;
      self.finishedUpgraders = null;
  
      // Initialized by initializeCatalog.
      self.projectCatalog = null;
      self.localCatalog = null;
      // Once the catalog is read and the names of the "explicitly
      // added" packages are determined, they will be listed here.
      // (See explicitlyAddedLocalPackageDirs.)
      // "Explicitly added" packages are typically present in non-app
      // projects, like the one created by `meteor publish`.  This list
      // is used to avoid pinning such packages to their previous
      // versions when we run the version solver, which prevents an
      // error telling you to pass `--allow-incompatible-update` when
      // you publish your package after bumping the major version.
      self.explicitlyAddedPackageNames = null;
  
      // Initialized by _resolveConstraints.
      self.packageMap = null;
      self.packageMapDelta = null;
  
      if (resetOptions.softRefreshIsopacks && self.isopackCache) {
        // Make sure we only hold on to one old isopack cache, not a linked list
        // of all of them.
        self.isopackCache.forgetPreviousIsopackCache();
        self._previousIsopackCache = self.isopackCache;
      } else {
        self._previousIsopackCache = null;
      }
      // Initialized by _buildLocalPackages.
      self.isopackCache = null;
  
      self._completedStage = STAGE.INITIAL;
  
      // The resolverResultCache is used by the constraint solver; to
      // us it's just an opaque object.  If we pass it into repeated
      // calls to the constraint solver, the constraint solver can be
      // more efficient by caching or memoizing its work.  We choose not
      // to reset this when reset() is called more than once.
      self._resolverResultCache = (self._resolverResultCache || {});
    },
  
    readProjectMetadata: function () {
      this._completeStagesThrough(STAGE.READ_PROJECT_METADATA);
    },
    initializeCatalog: function () {
      this._completeStagesThrough(STAGE.INITIALIZE_CATALOG);
    },
    resolveConstraints: function () {
      this._completeStagesThrough(STAGE.RESOLVE_CONSTRAINTS);
    },
    downloadMissingPackages: function () {
      this._completeStagesThrough(STAGE.DOWNLOAD_MISSING_PACKAGES);
    },
    buildLocalPackages: function () {
      this._completeStagesThrough(STAGE.BUILD_LOCAL_PACKAGES);
    },
    saveChangedMetadata: function () {
      this._completeStagesThrough(STAGE.SAVE_CHANGED_METADATA);
    },
    prepareProjectForBuild: function () {
      // This is the same as saveChangedMetadata, but if we insert stages after
      // that one it will continue to mean "fully finished".
      this.saveChangedMetadata();
    },
  
    _completeStagesThrough: function (targetStage) {
      var self = this;
      buildmessage.assertInCapture();
  
      buildmessage.enterJob('preparing project', function () {
        while (self._completedStage !== targetStage) {
          // This error gets thrown if you request to go to a stage that's earlier
          // than where you started. Note that the error will be mildly confusing
          // because the key of STAGE does not match the value.
          if (self.completedStage === STAGE.SAVE_CHANGED_METADATA)
            throw Error("can't find requested stage " + targetStage);
  
          // The actual value of STAGE.FOO is the name of the method that takes
          // you to the next step after FOO.
          self[self._completedStage]();
          if (buildmessage.jobHasMessages())
            return;
        }
      });
    },
  
    getProjectLocalDirectory: function (subdirectory) {
      var self = this;
      return files.pathJoin(self.projectDir, '.meteor', 'local', subdirectory);
    },
  
    getMeteorShellDirectory: function(projectDir) {
      return this.getProjectLocalDirectory("shell");
    },
  
    // You can call this manually if you want to do some work before resolving
    // constraints, or you can let prepareProjectForBuild do it for you.
    //
    // This should be pretty fast --- for example, we shouldn't worry about
    // needing to wait for it to be done before we open the runner proxy.
    _readProjectMetadata: function () {
      var self = this;
      buildmessage.assertInCapture();
  
      buildmessage.enterJob('reading project metadata', function () {
        // Ensure this is actually a project directory.
        self._ensureProjectDir();
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/release.
        self.releaseFile = new exports.ReleaseFile({
          projectDir: self.projectDir
        });
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/packages.
        self.projectConstraintsFile = new exports.ProjectConstraintsFile({
          projectDir: self.projectDir
        });
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/versions.
        self.packageMapFile = new exports.PackageMapFile({
          filename: self._packageMapFilename
        });
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/cordova-plugins.
        self.cordovaPluginsFile = new exports.CordovaPluginsFile({
          projectDir: self.projectDir
        });
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/platforms, creating it if necessary.
        self.platformList = new exports.PlatformList({
          projectDir: self.projectDir
        });
        if (buildmessage.jobHasMessages())
          return;
  
        // Read .meteor/.id, creating it if necessary.
        self._ensureAppIdentifier();
        if (buildmessage.jobHasMessages())
          return;
  
        // Set up an object that knows how to read and write
        // .meteor/.finished-upgraders.
        self.finishedUpgraders = new exports.FinishedUpgraders({
          projectDir: self.projectDir
        });
        if (buildmessage.jobHasMessages())
          return;
      });
  
      self._completedStage = STAGE.READ_PROJECT_METADATA;
    },
  
    _ensureProjectDir: function () {
      var self = this;
      files.mkdir_p(files.pathJoin(self.projectDir, '.meteor'));
  
      // This file existing is what makes a project directory a project directory,
      // so let's make sure it exists!
      var constraintFilePath = files.pathJoin(self.projectDir, '.meteor', 'packages');
      if (! files.exists(constraintFilePath)) {
        files.writeFileAtomically(constraintFilePath, '');
      }
  
      // Let's also make sure we have a minimal gitignore.
      var gitignorePath = files.pathJoin(self.projectDir, '.meteor', '.gitignore');
      if (! files.exists(gitignorePath)) {
        files.writeFileAtomically(gitignorePath, 'local\n');
      }
    },
  
    // This is a WatchSet that ends up being the WatchSet for the app's
    // initFromAppDir PackageSource. Changes to this will cause the whole app to
    // be rebuilt (client and server).
    getProjectWatchSet: function () {
      // We don't cache a projectWatchSet on this object, since some of the
      // metadata files can be written by us (eg .meteor/versions
      // post-constraint-solve).
      var self = this;
      var watchSet = new watch.WatchSet;
      _.each(
        [self.releaseFile, self.projectConstraintsFile, self.packageMapFile,
         self.platformList, self.cordovaPluginsFile],
        function (metadataFile) {
          metadataFile && watchSet.merge(metadataFile.watchSet);
        });
  
      if (self.localCatalog) {
        watchSet.merge(self.localCatalog.packageLocationWatchSet);
      }
  
      return watchSet;
    },
  
    // This WatchSet encompasses everything that users can change to restart an
    // app. We only watch this for failed bundles; for successful bundles, we have
    // more precise server-specific and client-specific WatchSets that add up to
    // this one.
    getProjectAndLocalPackagesWatchSet: function () {
      var self = this;
      var watchSet = self.getProjectWatchSet();
  
      // Include the loaded local packages (ie, the non-metadata files) but only
      // if we've actually gotten to the buildLocalPackages step.
      if (self.isopackCache) {
        watchSet.merge(self.isopackCache.allLoadedLocalPackagesWatchSet);
      }
      return watchSet;
    },
  
    getLintingMessagesForLocalPackages: function () {
      var self = this;
      return self.isopackCache.getLintingMessagesForLocalPackages();
    },
  
    _ensureAppIdentifier: function () {
      var self = this;
      var identifierFile = files.pathJoin(self.projectDir, '.meteor', '.id');
  
      // Find the first non-empty line, ignoring comments. We intentionally don't
      // put this in a WatchSet, since changing this doesn't affect the built app
      // much (and there's no real reason to update it anyway).
      var lines = files.getLinesOrEmpty(identifierFile);
      var appId = _.find(_.map(lines, files.trimSpaceAndComments), _.identity);
  
      // If the file doesn't exist or has no non-empty lines, regenerate the
      // token.
      if (!appId) {
        appId = utils.randomToken() + utils.randomToken() + utils.randomToken();
  
        var comment = (
  "# This file contains a token that is unique to your project.\n" +
  "# Check it into your repository along with the rest of this directory.\n" +
  "# It can be used for purposes such as:\n" +
  "#   - ensuring you don't accidentally deploy one app on top of another\n" +
  "#   - providing package authors with aggregated statistics\n" +
  "\n");
        files.writeFileAtomically(identifierFile, comment + appId + '\n');
      }
  
      self.appIdentifier = appId;
    },
  
    _resolveConstraints: function () {
      var self = this;
      buildmessage.assertInJob();
  
      var depsAndConstraints = self._getRootDepsAndConstraints();
      // If this is in the runner and we have reset this ProjectContext for a
      // rebuild, use the versions we calculated last time in this process (which
      // may not have been written to disk if our release doesn't match the
      // project's release on disk)... unless the actual file on disk has changed
      // out from under us. Otherwise use the versions from .meteor/versions.
      var cachedVersions;
      if (self._cachedVersionsBeforeReset &&
          self._oldPackageMapFileHash === self.packageMapFile.fileHash) {
        // The file on disk hasn't change; reuse last time's results.
        cachedVersions = self._cachedVersionsBeforeReset;
      } else {
        // We don't have a last time, or the file has changed; use
        // .meteor/versions.
        cachedVersions = self.packageMapFile.getCachedVersions();
      }
  
      var anticipatedPrereleases = self._getAnticipatedPrereleases(
        depsAndConstraints.constraints, cachedVersions);
  
      if (self.explicitlyAddedPackageNames.length) {
        cachedVersions = _.clone(cachedVersions);
        _.each(self.explicitlyAddedPackageNames, function (p) {
          delete cachedVersions[p];
        });
      }
  
      var resolverRunCount = 0;
  
      // Nothing before this point looked in the official or project catalog!
      // However, the resolver does, so it gets run in the retry context.
      catalog.runAndRetryWithRefreshIfHelpful(function (canRetry) {
        buildmessage.enterJob("selecting package versions", function () {
          var resolver = self._buildResolver();
  
          var resolveOptions = {
            previousSolution: cachedVersions,
            anticipatedPrereleases: anticipatedPrereleases,
            allowIncompatibleUpdate: self._allowIncompatibleUpdate,
            // Not finding an exact match for a previous version in the catalog
            // is considered an error if we haven't refreshed yet, and will
            // trigger a refresh and another attempt.  That way, if a previous
            // version exists, you'll get it, even if we don't have a record
            // of it yet.  It's not actually fatal, though, for previousSolution
            // to refer to package versions that we don't have access to or don't
            // exist.  They'll end up getting changed or removed if possible.
            missingPreviousVersionIsError: canRetry
          };
          if (self._upgradePackageNames) {
            resolveOptions.upgrade = self._upgradePackageNames;
          }
          if (self._upgradeIndirectDepPatchVersions) {
            resolveOptions.upgradeIndirectDepPatchVersions = true;
          }
  
          resolverRunCount++;
  
          var solution;
          try {
            Profile.run(
              "Select Package Versions" +
                (resolverRunCount > 1 ? (" (Try " + resolverRunCount + ")") : ""),
              function () {
                solution = resolver.resolve(
                  depsAndConstraints.deps, depsAndConstraints.constraints,
                  resolveOptions);
              });
          } catch (e) {
            if (!e.constraintSolverError && !e.versionParserError)
              throw e;
            // If the contraint solver gave us an error, refreshing
            // might help to get new packages (see the comment on
            // missingPreviousVersionIsError above).  If it's a
            // package-version-parser error, print a nice message,
            // but don't bother refreshing.
            buildmessage.error(
              e.message,
              { tags: { refreshCouldHelp: !!e.constraintSolverError }});
          }
  
          if (buildmessage.jobHasMessages())
            return;
  
          self.packageMap = new packageMapModule.PackageMap(solution.answer, {
            localCatalog: self.localCatalog
          });
  
          self.packageMapDelta = new packageMapModule.PackageMapDelta({
            cachedVersions: cachedVersions,
            packageMap: self.packageMap,
            usedRCs: solution.usedRCs,
            neededToUseUnanticipatedPrereleases:
            solution.neededToUseUnanticipatedPrereleases,
            anticipatedPrereleases: anticipatedPrereleases
          });
  
          self._completedStage = STAGE.RESOLVE_CONSTRAINTS;
        });
      });
    },
  
    _localPackageSearchDirs: function () {
      var self = this;
      var searchDirs = [files.pathJoin(self._projectDirForLocalPackages, 'packages')];
  
      if (! self._ignorePackageDirsEnvVar && process.env.PACKAGE_DIRS) {
        // User can provide additional package directories to search in
        // PACKAGE_DIRS (colon-separated).
        _.each(process.env.PACKAGE_DIRS.split(':'), function (p) {
          searchDirs.push(files.pathResolve(p));
        });
      }
  
      if (! self._ignoreCheckoutPackages && files.inCheckout()) {
        // Running from a checkout, so use the Meteor core packages from the
        // checkout.
        searchDirs.push(files.pathJoin(files.getCurrentToolsDir(), 'packages'));
      }
      return searchDirs;
    },
  
    // Returns a layered catalog with information about the packages that can be
    // used in this project. Processes the package.js file from all local packages
    // but does not compile the packages.
    //
    // Must be run in a buildmessage context. On build error, returns null.
    _initializeCatalog: function () {
      var self = this;
      buildmessage.assertInJob();
  
      catalog.runAndRetryWithRefreshIfHelpful(function () {
        buildmessage.enterJob(
          "scanning local packages",
          function () {
            self.localCatalog = new catalogLocal.LocalCatalog;
            self.projectCatalog = new catalog.LayeredCatalog(
              self.localCatalog, self._officialCatalog);
  
            var searchDirs = self._localPackageSearchDirs();
            self.localCatalog.initialize({
              localPackageSearchDirs: searchDirs,
              explicitlyAddedLocalPackageDirs: self._explicitlyAddedLocalPackageDirs
            });
  
            if (buildmessage.jobHasMessages()) {
              // Even if this fails, we want to leave self.localCatalog assigned,
              // so that it gets counted included in the projectWatchSet.
              return;
            }
  
            self.explicitlyAddedPackageNames = [];
            _.each(self._explicitlyAddedLocalPackageDirs, function (dir) {
              var localVersionRecord =
                    self.localCatalog.getVersionBySourceRoot(dir);
              if (localVersionRecord) {
                self.explicitlyAddedPackageNames.push(localVersionRecord.packageName);
              }
            });
  
            self._completedStage = STAGE.INITIALIZE_CATALOG;
          }
        );
      });
    },
  
    _getRootDepsAndConstraints: function () {
      var self = this;
  
      var depsAndConstraints = {deps: [], constraints: []};
  
      self._addAppConstraints(depsAndConstraints);
      self._addLocalPackageConstraints(depsAndConstraints);
      self._addReleaseConstraints(depsAndConstraints);
      return depsAndConstraints;
    },
  
    _addAppConstraints: function (depsAndConstraints) {
      var self = this;
  
      self.projectConstraintsFile.eachConstraint(function (constraint) {
        // Add a dependency ("this package must be used") and a constraint
        // ("... at this version (maybe 'any reasonable')").
        depsAndConstraints.deps.push(constraint.package);
        depsAndConstraints.constraints.push(constraint);
      });
    },
  
    _addLocalPackageConstraints: function (depsAndConstraints) {
      var self = this;
      _.each(self.localCatalog.getAllPackageNames(), function (packageName) {
        var versionRecord = self.localCatalog.getLatestVersion(packageName);
        var constraint = utils.parsePackageConstraint(
          packageName + "@=" + versionRecord.version);
        // Add a constraint ("this is the only version available") but no
        // dependency (we don't automatically use all local packages!)
        depsAndConstraints.constraints.push(constraint);
      });
    },
  
    _addReleaseConstraints: function (depsAndConstraints) {
      var self = this;
      if (! self._releaseForConstraints)
        return;
      _.each(self._releaseForConstraints.packages, function (version, packageName) {
        var constraint = utils.parsePackageConstraint(
          packageName + "@=" + version);
        // Add a constraint ("this is the only version available") but no
        // dependency (we don't automatically use all local packages!)
        depsAndConstraints.constraints.push(constraint);
      });
    },
  
    _getAnticipatedPrereleases: function (rootConstraints, cachedVersions) {
      var self = this;
  
      var anticipatedPrereleases = {};
      var add = function (packageName, version) {
        if (! /-/.test(version)) {
          return;
        }
        if (! _.has(anticipatedPrereleases, packageName)) {
          anticipatedPrereleases[packageName] = {};
        }
        anticipatedPrereleases[packageName][version] = true;
      };
  
      // Pre-release versions that are root constraints (in .meteor/packages, in
      // the release, or the version of a local package) are anticipated.
      _.each(rootConstraints, function (constraintObject) {
        _.each(constraintObject.versionConstraint.alternatives, function (alt) {
          var version = alt.versionString;
          version && add(constraintObject.package, version);
        });
      });
  
      // Pre-release versions we decided to use in the past are anticipated.
      _.each(cachedVersions, function (version, packageName) {
        add(packageName, version);
      });
  
      return anticipatedPrereleases;
    },
  
    _buildResolver: function () {
      var self = this;
  
      var constraintSolverPackage =
            isopackets.load('constraint-solver')['constraint-solver'];
      var resolver =
            new constraintSolverPackage.ConstraintSolver.PackagesResolver(
              self.projectCatalog, {
                nudge: function () {
                  Console.nudge(true);
                },
                Profile: Profile,
                resultCache: self._resolverResultCache
              });
      return resolver;
    },
  
    _downloadMissingPackages: function () {
      var self = this;
      buildmessage.assertInJob();
      if (!self.packageMap)
        throw Error("which packages to download?");
  
      catalog.runAndRetryWithRefreshIfHelpful(function () {
        buildmessage.enterJob("downloading missing packages", function () {
          self.tropohouse.downloadPackagesMissingFromMap(self.packageMap, {
            serverArchitectures: self._serverArchitectures
          });
          if (buildmessage.jobHasMessages())
            return;
          self._completedStage = STAGE.DOWNLOAD_MISSING_PACKAGES;
        });
      });
    },
  
    _buildLocalPackages: function () {
      var self = this;
      buildmessage.assertInCapture();
  
      self.isopackCache = new isopackCacheModule.IsopackCache({
        packageMap: self.packageMap,
        includeCordovaUnibuild: (self._forceIncludeCordovaUnibuild
                                 || self.platformList.usesCordova()),
        cacheDir: self.getProjectLocalDirectory('isopacks'),
        pluginCacheDirRoot: self.getProjectLocalDirectory('plugin-cache'),
        tropohouse: self.tropohouse,
        previousIsopackCache: self._previousIsopackCache,
        lintLocalPackages: self.lintAppAndLocalPackages,
        lintPackageWithSourceRoot: self._lintPackageWithSourceRoot
      });
  
      if (self._forceRebuildPackages) {
        self.isopackCache.wipeCachedPackages(
          self._forceRebuildPackages === true
            ? null : self._forceRebuildPackages);
      }
  
      buildmessage.enterJob('building local packages', function () {
        self.isopackCache.buildLocalPackages();
      });
      self._completedStage = STAGE.BUILD_LOCAL_PACKAGES;
    },
  
    _saveChangedMetadata: function () {
      var self = this;
  
      // Save any changes to .meteor/packages.
      if (! self._neverWriteProjectConstraintsFile)
        self.projectConstraintsFile.writeIfModified();
  
      // Write .meteor/versions if the command always wants to (create/update),
      // or if the release of the app matches the release of the process.
      if (! self._neverWritePackageMap &&
          (self._alwaysWritePackageMap ||
           (release.current.isCheckout() && self.releaseFile.isCheckout()) ||
           (! release.current.isCheckout() &&
            release.current.name === self.releaseFile.fullReleaseName))) {
  
        self.packageMapFile.write(self.packageMap);
      }
  
      self._completedStage = STAGE.SAVE_CHANGED_METADATA;
    }
  });
  
  
  // Represents .meteor/packages.
  exports.ProjectConstraintsFile = function (options) {
    var self = this;
    buildmessage.assertInCapture();
  
    self.filename = files.pathJoin(options.projectDir, '.meteor', 'packages');
    self.watchSet = null;
  
    // Have we modified the in-memory representation since reading from disk?
    self._modified = null;
    // List of each line in the file; object with keys:
    // - leadingSpace (string of spaces before the constraint)
    // - constraint (as returned by utils.parsePackageConstraint)
    // - trailingSpaceAndComment (string of spaces/comments after the constraint)
    // This allows us to rewrite the file preserving comments.
    self._constraintLines = null;
    // Maps from package name to entry in _constraintLines.
    self._constraintMap = null;
    self._readFile();
  };
  
  _.extend(exports.ProjectConstraintsFile.prototype, {
    _readFile: function () {
      var self = this;
      buildmessage.assertInCapture();
  
      self.watchSet = new watch.WatchSet;
      self._modified = false;
      self._constraintMap = {};
      self._constraintLines = [];
      var contents = watch.readAndWatchFile(self.watchSet, self.filename);
  
      // No .meteor/packages? This isn't a very good project directory. In fact,
      // that's the definition of a project directory! (And that should have been
      // fixed by _ensureProjectDir!)
      if (contents === null)
        throw Error("packages file missing: " + self.filename);
  
      var lines = files.splitBufferToLines(contents);
      // Don't keep a record for the space at the end of the file.
      if (lines.length && _.last(lines) === '')
        lines.pop();
  
      _.each(lines, function (line) {
        var lineRecord =
              { leadingSpace: '', constraint: null, trailingSpaceAndComment: '' };
        self._constraintLines.push(lineRecord);
        // Strip comment.
        var match = line.match(/^([^#]*)(#.*)$/);
        if (match) {
          line = match[1];
          lineRecord.trailingSpaceAndComment = match[2];
        }
        // Strip trailing space.
        match = line.match(/^((?:.*\S)?)(\s*)$/);
        line = match[1];
        lineRecord.trailingSpaceAndComment =
          match[2] + lineRecord.trailingSpaceAndComment;
        // Strip leading space.
        match = line.match(/^(\s*)((?:\S.*)?)$/);
        lineRecord.leadingSpace = match[1];
        line = match[2];
  
        // No constraint? Leave lineRecord.constraint null and continue.
        if (line === '')
          return;
        lineRecord.constraint = utils.parsePackageConstraint(line, {
          useBuildmessage: true,
          buildmessageFile: self.filename
        });
        if (! lineRecord.constraint)
          return;  // recover by ignoring
  
        if (_.has(self._constraintMap, lineRecord.constraint.package)) {
          buildmessage.error(
            "Package name appears twice: " + lineRecord.constraint.package, {
              // XXX should this be relative?
              file: self.filename
            });
          return;  // recover by ignoring
        }
        self._constraintMap[lineRecord.constraint.package] = lineRecord;
      });
    },
  
    writeIfModified: function () {
      var self = this;
      self._modified && self._write();
    },
  
    _write: function () {
      var self = this;
      var lines = _.map(self._constraintLines, function (lineRecord) {
        var lineParts = [lineRecord.leadingSpace];
        if (lineRecord.constraint) {
          lineParts.push(lineRecord.constraint.package);
          if (lineRecord.constraint.constraintString) {
            lineParts.push('@', lineRecord.constraint.constraintString);
          }
        }
        lineParts.push(lineRecord.trailingSpaceAndComment, '\n');
        return lineParts.join('');
      });
      files.writeFileAtomically(self.filename, lines.join(''));
      var messages = buildmessage.capture(
        { title: 're-reading .meteor/packages' },
        function () {
          self._readFile();
        });
      // We shouldn't choke on something we just wrote!
      if (messages.hasMessages())
        throw Error("wrote bad .meteor/packages: " + messages.formatMessages());
    },
  
    // Iterates over all constraints, in the format returned by
    // utils.parsePackageConstraint.
    eachConstraint: function (iterator) {
      var self = this;
      _.each(self._constraintLines, function (lineRecord) {
        if (lineRecord.constraint)
          iterator(lineRecord.constraint);
      });
    },
  
    // Returns the constraint in the format returned by
    // utils.parsePackageConstraint, or null.
    getConstraint: function (name) {
      var self = this;
      if (_.has(self._constraintMap, name))
        return self._constraintMap[name].constraint;
      return null;
    },
  
    // Adds constraints, an array of objects as returned from
    // utils.parsePackageConstraint.
    // Does not write to disk immediately; changes are written to disk by
    // writeIfModified() which is called in the _saveChangedMetadata step
    // of project preparation.
    addConstraints: function (constraintsToAdd) {
      var self = this;
      _.each(constraintsToAdd, function (constraintToAdd) {
        if (! constraintToAdd.package) {
          throw new Error("Expected PackageConstraint: " + constraintToAdd);
        }
  
        var lineRecord;
        if (! _.has(self._constraintMap, constraintToAdd.package)) {
          lineRecord = {
            leadingSpace: '',
            constraint: constraintToAdd,
            trailingSpaceAndComment: ''
          };
          self._constraintLines.push(lineRecord);
          self._constraintMap[constraintToAdd.package] = lineRecord;
          self._modified = true;
          return;
        }
        lineRecord = self._constraintMap[constraintToAdd.package];
        if (_.isEqual(constraintToAdd, lineRecord.constraint))
          return;  // nothing changed
        lineRecord.constraint = constraintToAdd;
        self._modified = true;
      });
    },
  
    // Like addConstraints, but takes an array of package name strings
    // to add with no version constraint
    addPackages: function (packagesToAdd) {
      this.addConstraints(_.map(packagesToAdd, function (packageName) {
        // make sure packageName is valid (and doesn't, for example,
        // contain an '@' sign)
        utils.validatePackageName(packageName);
        return utils.parsePackageConstraint(packageName);
      }));
    },
  
    // The packages in packagesToRemove are expected to actually be in the file;
    // if you want to provide different output for packages in the file vs not,
    // you should have already done that.
    // Does not write to disk immediately; changes are written to disk by
    // writeIfModified() which is called in the _saveChangedMetadata step
    // of project preparation.
    removePackages: function (packagesToRemove) {
      var self = this;
      self._constraintLines = _.filter(
        self._constraintLines, function (lineRecord) {
          return ! (lineRecord.constraint &&
                    _.contains(packagesToRemove, lineRecord.constraint.package));
        });
      _.each(packagesToRemove, function (p) {
        delete self._constraintMap[p];
      });
      self._modified = true;
    },
  
    // Removes all constraints. Generally this should only be used in situations
    // where the project is not a real user app: while you can use
    // removeAllPackages followed by addConstraints to fully replace the
    // constraints in a project, this will also lose all user comments and
    // (cosmetic) ordering from the file.
    removeAllPackages: function () {
      var self = this;
      self._constraintLines = [];
      self._constraintMap = {};
      self._modified = true;
    }
  });
  
  
  
  // Represents .meteor/versions.
  exports.PackageMapFile = function (options) {
    var self = this;
    buildmessage.assertInCapture();
  
    self.filename = options.filename;
    self.watchSet = new watch.WatchSet;
    self.fileHash = null;
    self._versions = {};
  
    self._readFile();
  };
  
  _.extend(exports.PackageMapFile.prototype, {
    _readFile: function () {
      var self = this;
  
      var fileInfo = watch.readAndWatchFileWithHash(self.watchSet, self.filename);
      var contents = fileInfo.contents;
      self.fileHash = fileInfo.hash;
      // No .meteor/versions? That's OK, you just get to start your calculation
      // from scratch.
      if (contents === null)
        return;
  
      buildmessage.assertInCapture();
      var lines = files.splitBufferToLines(contents);
      _.each(lines, function (line) {
        // We don't allow comments here, since it's cruel to allow comments in a
        // file when you're going to overwrite them anyway.
        line = files.trimSpace(line);
        if (line === '')
          return;
        var packageVersion = utils.parsePackageAndVersion(line, {
          useBuildmessage: true,
          buildmessageFile: self.filename
        });
        if (!packageVersion)
          return;  // recover by ignoring
  
        // If a package appears multiple times in .meteor/versions, we just ignore
        // the second one. This file is more meteor-controlled than
        // .meteor/packages and people shouldn't be surprised to see it
        // automatically fixed.
        if (_.has(self._versions, packageVersion.package))
          return;
  
        self._versions[packageVersion.package] = packageVersion.version;
      });
    },
  
    // Note that this is really specific to wanting to know what versions are in
    // the .meteor/versions file on disk, which is a slightly different question
    // from "so, what versions should I be building with?"  Usually you want the
    // PackageMap produced by resolving constraints instead! Returns a map from
    // package name to version.
    getCachedVersions: function () {
      var self = this;
      return _.clone(self._versions);
    },
  
    write: function (packageMap) {
      var self = this;
      var newVersions = packageMap.toVersionMap();
  
      // Only write the file if some version changed. (We don't need to do no-op
      // writes, even if they fix sorting in the file.)
      if (_.isEqual(self._versions, newVersions))
        return;
  
      self._versions = newVersions;
      var packageNames = _.keys(self._versions);
      packageNames.sort();
      var lines = [];
      _.each(packageNames, function (packageName) {
        lines.push(packageName + "@" + self._versions[packageName] + "\n");
      });
      var fileContents = new Buffer(lines.join(''));
      files.writeFileAtomically(self.filename, fileContents);
  
      // Replace our watchSet with one for the new contents of the file.
      var hash = watch.sha1(fileContents);
      self.watchSet = new watch.WatchSet;
      self.watchSet.addFile(self.filename, hash);
    }
  });
  
  
  
  // Represents .meteor/platforms. We take no effort to maintain comments or
  // spacing here.
  exports.PlatformList = function (options) {
    var self = this;
  
    self.filename = files.pathJoin(options.projectDir, '.meteor', 'platforms');
    self.watchSet = null;
    self._platforms = null;
  
    self._readFile();
  };
  
  // These platforms are always present and can be neither added or removed
  exports.PlatformList.DEFAULT_PLATFORMS = ['browser', 'server'];
  
  _.extend(exports.PlatformList.prototype, {
    _readFile: function () {
      var self = this;
  
      // Reset the WatchSet.
      self.watchSet = new watch.WatchSet;
      var contents = watch.readAndWatchFile(self.watchSet, self.filename);
  
      var platforms = contents ? files.splitBufferToLines(contents) : [];
      // We don't allow comments here, since it's cruel to allow comments in a
      // file when you're going to overwrite them anyway.
      platforms = _.uniq(_.compact(_.map(platforms, files.trimSpace)));
      platforms.sort();
  
      // Missing some of the default platforms (or the whole file)? Add them and
      // try again.
      if (_.difference(exports.PlatformList.DEFAULT_PLATFORMS,
                       platforms).length) {
        // Write the platforms to disk (automatically adding DEFAULT_PLATFORMS and
        // sorting), which automatically calls this function recursively to
        // re-reads them.
        self.write(platforms);
        return;
      }
  
      self._platforms = platforms;
    },
  
    // Replaces the current platform file with the given list and resets this
    // object (and its WatchSet) to track the new value.
    write: function (platforms) {
      var self = this;
      self._platforms = null;
      platforms = _.uniq(
        platforms.concat(exports.PlatformList.DEFAULT_PLATFORMS));
      platforms.sort();
      files.writeFileAtomically(self.filename, platforms.join('\n') + '\n');
      self._readFile();
    },
  
    getPlatforms: function () {
      var self = this;
      return _.clone(self._platforms);
    },
  
    getCordovaPlatforms: function () {
      var self = this;
      return _.difference(self._platforms,
                          exports.PlatformList.DEFAULT_PLATFORMS);
    },
  
    usesCordova: function () {
      var self = this;
      return ! _.isEmpty(self.getCordovaPlatforms());
    },
  
    getWebArchs: function () {
      var self = this;
      var archs = [ "web.browser" ];
      if (self.usesCordova()) {
        archs.push("web.cordova");
      }
      return archs;
    }
  });
  
  
  // Represents .meteor/cordova-plugins.
  exports.CordovaPluginsFile = function (options) {
    var self = this;
    buildmessage.assertInCapture();
  
    self.filename = files.pathJoin(options.projectDir, '.meteor', 'cordova-plugins');
    self.watchSet = null;
    // Map from plugin name to version.
    self._plugins = null;
  
    self._readFile();
  };
  
  _.extend(exports.CordovaPluginsFile.prototype, {
    _readFile: function () {
      var self = this;
      buildmessage.assertInCapture();
  
      self.watchSet = new watch.WatchSet;
      self._plugins = {};
      var contents = watch.readAndWatchFile(self.watchSet, self.filename);
      // No file?  No plugins.
      if (contents === null)
        return;
  
      var lines = files.splitBufferToLines(contents);
      _.each(lines, function (line) {
        line = files.trimSpace(line);
        if (line === '')
          return;
  
        // We just do a standard split here, not utils.parsePackageConstraint,
        // since cordova plugins don't necessary obey the same naming conventions
        // as Meteor packages.
        var parts = line.split('@');
        if (parts.length !== 2) {
          buildmessage.error("Cordova plugin must specify version: " + line, {
            // XXX should this be relative?
            file: self.filename
          });
          return;  // recover by ignoring
        }
        if (_.has(self._plugins, parts[0])) {
          buildmessage.error("Plugin name appears twice: " + parts[0], {
            // XXX should this be relative?
            file: self.filename
          });
          return;  // recover by ignoring
        }
        self._plugins[parts[0]] = parts[1];
      });
    },
  
    getPluginVersions: function () {
      var self = this;
      return _.clone(self._plugins);
    },
  
    write: function (plugins) {
      var self = this;
      var pluginNames = _.keys(plugins);
      pluginNames.sort();
      var lines = _.map(pluginNames, function (pluginName) {
        return pluginName + '@' + plugins[pluginName] + '\n';
      });
      files.writeFileAtomically(self.filename, lines.join(''));
      var messages = buildmessage.capture(
        { title: 're-reading .meteor/cordova-plugins' },
        function () {
          self._readFile();
        });
      // We shouldn't choke on something we just wrote!
      if (messages.hasMessages())
        throw Error("wrote bad .meteor/packages: " + messages.formatMessages());
    }
  });
  
  
  
  // Represents .meteor/release.
  exports.ReleaseFile = function (options) {
    var self = this;
  
    self.filename = files.pathJoin(options.projectDir, '.meteor', 'release');
    self.watchSet = null;
    // The release name actually written in the file.  Null if no fill.  Empty if
    // the file is empty.
    self.unnormalizedReleaseName = null;
    // The full release name (with METEOR@ if it's missing in
    // unnormalizedReleaseName).
    self.fullReleaseName = null;
    // FOO@bar unless FOO === "METEOR" in which case "Meteor bar".
    self.displayReleaseName = null;
    // Just the track.
    self.releaseTrack = null;
    self.releaseVersion = null;
    self._readFile();
  };
  
  _.extend(exports.ReleaseFile.prototype, {
    fileMissing: function () {
      var self = this;
      return self.unnormalizedReleaseName === null;
    },
    noReleaseSpecified: function () {
      var self = this;
      return self.unnormalizedReleaseName === '';
    },
    isCheckout: function () {
      var self = this;
      return self.unnormalizedReleaseName === 'none';
    },
    normalReleaseSpecified: function () {
      var self = this;
      return ! (self.fileMissing() || self.noReleaseSpecified()
                || self.isCheckout());
    },
  
    _readFile: function () {
      var self = this;
  
      // Start a new watchSet, in case we just overwrote this.
      self.watchSet = new watch.WatchSet;
      var contents = watch.readAndWatchFile(self.watchSet, self.filename);
      // If file doesn't exist, leave unnormalizedReleaseName empty; fileMissing
      // will be true.
      if (contents === null)
        return;
  
      var lines = _.compact(_.map(files.splitBufferToLines(contents),
                                  files.trimSpaceAndComments));
      // noReleaseSpecified will be true.
      if (!lines.length) {
        self.unnormalizedReleaseName = '';
        return;
      }
  
      self.unnormalizedReleaseName = lines[0];
  
      const catalogUtils = require('./packaging/catalog/catalog-utils.js');
      var parts = catalogUtils.splitReleaseName(self.unnormalizedReleaseName);
      self.fullReleaseName = parts[0] + '@' + parts[1];
      self.displayReleaseName = catalogUtils.displayRelease(parts[0], parts[1]);
      self.releaseTrack = parts[0];
      self.releaseVersion = parts[1];
    },
  
    write: function (releaseName) {
      var self = this;
      files.writeFileAtomically(self.filename, releaseName + '\n');
      self._readFile();
    }
  });
  
  
  // Represents .meteor/.finished-upgraders.
  // This is only used in a few places, so we don't cache its value in memory;
  // we just read it when we need it. There's also no need to add it to a
  // watchSet because we don't need to rebuild when it changes.
  exports.FinishedUpgraders = function (options) {
    var self = this;
  
    self.filename = files.pathJoin(
      options.projectDir, '.meteor', '.finished-upgraders');
  };
  
  _.extend(exports.FinishedUpgraders.prototype, {
    readUpgraders: function () {
      var self = this;
      var upgraders = [];
      var lines = files.getLinesOrEmpty(self.filename);
      _.each(lines, function (line) {
        line = files.trimSpaceAndComments(line);
        if (line === '')
          return;
        upgraders.push(line);
      });
      return upgraders;
    },
  
    appendUpgraders: function (upgraders) {
      var self = this;
  
      var current = null;
      try {
        current = files.readFile(self.filename, 'utf8');
      } catch (e) {
        if (e.code !== 'ENOENT')
          throw e;
      }
  
      var appendText = '';
      if (current === null) {
        // We're creating this file for the first time. Include a helpful comment.
        appendText =
  "# This file contains information which helps Meteor properly upgrade your\n" +
  "# app when you run 'meteor update'. You should check it into version control\n" +
  "# with your project.\n" +
  "\n";
      } else if (current.length && current[current.length - 1] !== '\n') {
        // File has an unterminated last line. Let's terminate it.
        appendText = '\n';
      }
  
      _.each(upgraders, function (upgrader) {
        appendText += upgrader + '\n';
      });
  
      files.appendFile(self.filename, appendText);
    }
  });
  