var _ = require('underscore');
var fs = require('fs');
var path = require('path');

var archinfo = require('./archinfo.js');
var buildmessage = require('./buildmessage.js');
var catalog = require('./catalog.js');
var catalogLocal = require('./catalog-local.js');
var catalogRemote = require('./catalog-remote.js');
var Console = require('./console.js').Console;
var files = require('./files.js');
var isopackCacheModule = require('./isopack-cache.js');
var isopackets = require('./isopackets.js');
var packageMapModule = require('./package-map.js');
var release = require('./release.js');
var tropohouse = require('./tropohouse.js');
var utils = require('./utils.js');
var watch = require('./watch.js');

// 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.
exports.ProjectContext = function (options) {
  var self = this;
  if (!options.projectDir)
    throw Error("missing projectDir!");

  self.originalOptions = options;
  self.reset();
};

// 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(exports.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 ||
      path.join(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.
    self._forceRebuildPackages = options.forceRebuildPackages;

    // 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 in a few cases where we really want to only get packages from
    // checkout.
    self._ignorePackageDirsEnvVar = options.ignorePackageDirsEnvVar;

    // Set by 'meteor publish' to ensure that .meteor/packages is not written
    // even though they may do an in-memory mutation of the constraints.
    self._neverWriteProjectConstraintsFile =
      options.neverWriteProjectConstraintsFile;

    // Set by 'meteor update' to specify which packages may be updated. Array of
    // package names.
    self._upgradePackageNames = options.upgradePackageNames;

    // Set when deploying to a previous Galaxy prototype.
    self._requireControlProgram = options.requireControlProgram;

    // 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();
    } else {
      self._cachedVersionsBeforeReset = null;
    }

    // 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;

    // 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;
  },

  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 path.join(self.projectDir, '.meteor', 'local', subdirectory);
  },

  // 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(path.join(self.projectDir, '.meteor'));

    // This file existing is what makes a project directory a project directory,
    // so let's make sure it exists!
    var constraintFilePath = path.join(self.projectDir, '.meteor', 'packages');
    if (! fs.existsSync(constraintFilePath)) {
      files.writeFileAtomically(constraintFilePath, '');
    }

    // Let's also make sure we have a minimal gitignore.
    var gitignorePath = path.join(self.projectDir, '.meteor', '.gitignore');
    if (! fs.existsSync(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;
  },

  _ensureAppIdentifier: function () {
    var self = this;
    var identifierFile = path.join(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.assertInCapture();

    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). Otherwise use the versions from
    // .meteor/versions.
    var cachedVersions = self._cachedVersionsBeforeReset ||
          self.packageMapFile.getCachedVersions();
    var anticipatedPrereleases = self._getAnticipatedPrereleases(
      depsAndConstraints.constraints, cachedVersions);
    var resolver = self._buildResolver();

    var solution;
    buildmessage.enterJob("selecting package versions", function () {
      var resolveOptions = {
        previousSolution: cachedVersions,
        anticipatedPrereleases: anticipatedPrereleases
      };
      if (self._upgradePackageNames)
        resolveOptions.upgrade = self._upgradePackageNames;

      try {
        solution = resolver.resolve(
          depsAndConstraints.deps, depsAndConstraints.constraints,
          resolveOptions);
      } catch (e) {
        if (!e.constraintSolverError)
          throw e;
        buildmessage.error(e.message);
      }
    });

    if (!solution)
      return;  // error is already in buildmessage

    self.packageMap = new packageMapModule.PackageMap(
      solution.answer, 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 = [path.join(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(path.resolve(p));
      });
    }

    if (files.inCheckout()) {
      // Running from a checkout, so use the Meteor core packages from the
      // checkout.
      searchDirs.push(path.join(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.assertInCapture();

    self.localCatalog = new catalogLocal.LocalCatalog;
    self.projectCatalog = new catalog.LayeredCatalog(
      self.localCatalog, catalog.official);

    var searchDirs = self._localPackageSearchDirs();
    buildmessage.enterJob({ title: "scanning local packages" }, function () {
      self.localCatalog.initialize({
        localPackageSearchDirs: searchDirs,
        explicitlyAddedLocalPackageDirs: self._explicitlyAddedLocalPackageDirs
      });
    });

    self._completedStage = STAGE.INITIALIZE_CATALOG;
  },

  _getRootDepsAndConstraints: function () {
    var self = this;

    var depsAndConstraints = {deps: [], constraints: []};

    self._addAppConstraints(depsAndConstraints);
    self._addLocalPackageConstraints(depsAndConstraints);
    self._addReleaseConstraints(depsAndConstraints);
    self._addGalaxyPrototypeConstraints(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.name);
      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.parseConstraint(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.parseConstraint(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);
    });
  },

  // We only need to build ctl if deploying to the legacy Galaxy
  // prototype. (Note that this means that we will need a new constraint
  // solution when deploying vs when running locally. This code will be deleted
  // soon anyway.)
  _addGalaxyPrototypeConstraints: function (depsAndConstraints) {
    var self = this;
    if (self._requireControlProgram) {
      depsAndConstraints.deps.push('ctl');
    }
  },

  _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.constraints, function (alternative) {
        var version = alternative.version;
        version && add(constraintObject.name, 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);
        }
      });
    return resolver;
  },

  _downloadMissingPackages: function () {
    var self = this;
    buildmessage.assertInCapture();
    if (!self.packageMap)
      throw Error("which packages to download?");
    self.tropohouse.downloadPackagesMissingFromMap(self.packageMap, {
      serverArchitectures: self._serverArchitectures
    });
    self._completedStage = STAGE.DOWNLOAD_MISSING_PACKAGES;
  },

  _buildLocalPackages: function () {
    var self = this;
    buildmessage.assertInCapture();

    self.isopackCache = new isopackCacheModule.IsopackCache({
      packageMap: self.packageMap,
      cacheDir: self.getProjectLocalDirectory('isopacks'),
      tropohouse: self.tropohouse,
      previousIsopackCache: self._previousIsopackCache
    });

    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._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 = path.join(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.parseConstraint)
  // - 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.parseConstraint(line, {
        useBuildmessage: true,
        buildmessageFile: self.filename
      });
      if (! lineRecord.constraint)
        return;  // recover by ignoring

      if (_.has(self._constraintMap, lineRecord.constraint.name)) {
        buildmessage.error(
          "Package name appears twice: " + lineRecord.constraint.name, {
            // XXX should this be relative?
            file: self.filename
          });
        return;  // recover by ignoring
      }
      self._constraintMap[lineRecord.constraint.name] = 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.name);
        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.parseConstraint.
  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.parseConstraint, 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.parseConstraint.
  // 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) {
      var lineRecord;
      if (! _.has(self._constraintMap, constraintToAdd.name)) {
        lineRecord = {
          leadingSpace: '',
          constraint: constraintToAdd,
          trailingSpaceAndComment: ''
        };
        self._constraintLines.push(lineRecord);
        self._constraintMap[constraintToAdd.name] = lineRecord;
        self._modified = true;
        return;
      }
      lineRecord = self._constraintMap[constraintToAdd.name];
      if (_.isEqual(constraintToAdd, lineRecord.constraint))
        return;  // nothing changed
      lineRecord.constraint = constraintToAdd;
      self._modified = true;
    });
  },

  // 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.name));
      });
    _.each(packagesToRemove, function (p) {
      delete self._constraintMap[p];
    });
    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._versions = {};

  self._readFile();
};

_.extend(exports.PackageMapFile.prototype, {
  _readFile: function () {
    var self = this;

    var contents = watch.readAndWatchFile(self.watchSet, self.filename);
    // 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 constraint = utils.parseConstraint(line, {
        useBuildmessage: true,
        buildmessageFile: self.filename
      });
      if (!constraint)
        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, constraint.name))
        return;

      // We expect this constraint to be "foo@1.2.3", not a lack of a constraint
      // or something with "||" or "@=".
      if (! utils.isSimpleConstraint(constraint)) {
        buildmessage.error("Bad version: " + line, {
          // XXX should this be relative?
          file: self.filename
        });
        return;  // recover by ignoring
      }

      self._versions[constraint.name] = constraint.constraints[0].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 = path.join(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);
  },

  getWebArchs: function () {
    var self = this;
    var archs = [ "web.browser" ];
    if (! _.isEmpty(self.getCordovaPlatforms())) {
      archs.push("web.cordova");
    }
    return archs;
  }
});

// Represents .meteor/cordova-plugins.
exports.CordovaPluginsFile = function (options) {
  var self = this;
  buildmessage.assertInCapture();

  self.filename = path.join(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.trimSpaceAndComments(line);
      if (line === '')
        return;

      // We just do a standard split here, not utils.parseConstraint, 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 = path.join(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];
    var parts = utils.splitReleaseName(self.unnormalizedReleaseName);
    self.fullReleaseName = parts[0] + '@' + parts[1];
    self.displayReleaseName = utils.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 = path.join(
    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 = fs.readFileSync(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';
    });

    fs.appendFileSync(self.filename, appendText);
  }
});


// test single character prerelease parts

// Our broken implementation of Rule 11 (see [*] above the

// declaration of PackageVersion.versionMagnitude). Maybe one day

// we'll fix it, in which case replace "===" with ">"

keep keep keep keep add keep keep keep keep keep

// prerelease parts can contain digits and non-digits


// (Or it could just return some sort of tuple, and ensure that

// the cost functions that consume this can deal with tuples...)

// This file is in tools/package-version-parser.js and is symlinked into
// packages/package-version-parser/package-version-parser.js. It's part of both
// the tool and the package!  We don't use an isopacket for it because it used
// to be required as part of building isopackets (though that may no longer be
// true).
var inTool = typeof Package === 'undefined';

var PV;
if (inTool) {
  PV = exports;
} else {
  PackageVersion = PV = {};
}

var semver = inTool ? require ('semver') : Npm.require('semver');
var __ = inTool ? require('underscore') : _;

// Takes in a meteor version, for example 1.2.3-rc5_1+12345.
//
// Returns an object composed of the following:
//   semver: (ex: 1.2.3)
//   wrapNum: 0 or a valid wrap number.
//
// Throws if the wrapNumber is invalid, or if the version cannot be split
// reasonably.
var extractSemverPart = function (versionString) {
  if (!versionString) return { semver: "", wrapNum: -1 };
  var noBuild = versionString.split('+');
  var splitVersion = noBuild[0].split('_');
  var wrapNum = 0;
  // If we find two +s, or two _, that's super invalid.
  if (noBuild.length > 2 || splitVersion.length > 2) {
    throwVersionParserError(
      "Version string must look like semver (eg '1.2.3'), not '"
        + versionString + "'.");
  } else if (splitVersion.length > 1) {
    wrapNum = splitVersion[1];
    if (!/^\d+$/.test(wrapNum)) {
      throwVersionParserError(
        "The wrap number (after _) must contain only digits, so " +
          versionString + " is invalid.");
    } else if (wrapNum[0] === "0") {
      throwVersionParserError(
        "The wrap number (after _) must not have a leading zero, so " +
          versionString + " is invalid.");
    }
  }
  return {
    semver: (noBuild.length > 1) ?
      splitVersion[0] + "+" + noBuild[1] :
      splitVersion[0],
    wrapNum: parseInt(wrapNum, 10)
  };
};

// Converts a meteor version into a large floating point number, which
// is (more or less [*]) unique to that version. Satisfies the
// following guarantee: If PV.lessThan(v1, v2) then
// PV.versionMagnitude(v1) < PV.versionMagnitude(v2) [*]
//
// [* XXX!] We don't quite satisfy the uniqueness and comparison properties in
// these cases:
// 1. If any of the version parts are greater than 100 (pretty unlikely?)
// 2. If we're dealing with a prerelease version, we only look at the
//    first two characters of each prerelease part. So, "1.0.0-beta" and
//    "1.0.0-bear" will have the same magnitude.
// 3. If we're dealing with a prerelease version with more than two parts, eg
//    "1.0.0-rc.0.1". In this comparison may fail since we'd get to the limit
//    of JavaScript floating point precision.
//
// If we wanted to fix this, we'd make this function return a BigFloat
// instead of a vanilla JavaScript number. That will make the
// constraint solver slower (by how much?), and would require some
// careful thought.
PV.versionMagnitude = function (versionString) {
  var version = extractSemverPart(versionString);
  var v = semver.parse(version.semver);

  return v.major * 100 * 100 +
    v.minor * 100 +
    v.patch +
    version.wrapNum / 100 +
    prereleaseIdentifierToFraction(v.prerelease) / 100 / 100;
};

// Accepts an array, eg ["rc", 2, 3]. Returns a number in the range
// (-1, 0].  An empty array returns 0. A non-empty string returns a
// number that is "as large" as the its precedence.
var prereleaseIdentifierToFraction = function (prerelease) {
  if (prerelease.length === 0)
    return 0;

  return __.reduce(prerelease, function (memo, part, index) {
    var digit;
    if (typeof part === 'number') {
      digit = part+1;
    } else if (typeof part === 'string') {
      var VALID_CHARACTERS = "-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

      var validCharToNumber = function (ch) {
        var result = VALID_CHARACTERS.indexOf(ch);
        if (result === -1)
          throw new Error("Unexpected character in prerelease identifier: " + ch);
        else
          return result;
      };

      digit = 101 + // Numeric parts always have lower precedence than non-numeric parts.
        validCharToNumber(part[0]) * VALID_CHARACTERS.length +
        (part[1] ? validCharToNumber(part[1]) : 0);
    } else {
      throw new Error("Unexpected prerelease identifier part: " + part + " of type " + typeof part);
    }

    // 3000 > 101 + VALID_CHARACTERS.length *
    // VALID_CHARACTERS.length. And there's a test to verify this
    // ("test the edges of `versionMagnitude`")
    return memo + digit / Math.pow(3000, index+1);
  }, -1);
};

// Takes in two meteor versions. Returns true if the first one is less than the second.
PV.lessThan = function (versionOne, versionTwo) {
  return PV.compare(versionOne, versionTwo) < 0;
};

// Given a string version, returns its major version (the first section of the
// semver), as an integer. Two versions are compatible if they have the same
// version number.
//
// versionString: valid meteor version string.
PV.majorVersion = function (versionString) {
  var version = extractSemverPart(versionString).semver;
  var parsed = semver.parse(version);
  if (! parsed)
    throwVersionParserError("not a valid version: " + version);
  return parsed.major;
};

// Takes in two meteor versions. Returns 0 if equal, 1 if v1 is greater, -1 if
// v2 is greater.
PV.compare = function (versionOne, versionTwo) {
  var meteorVOne = extractSemverPart(versionOne);
  var meteorVTwo = extractSemverPart(versionTwo);

  // Wrap numbers only matter if the semver is equal, so if they don't even have
  // wrap numbers, or if their semver is not equal, then we should let the
  // semver library resolve this one.
  if (meteorVOne.semver !== meteorVTwo.semver) {
    return semver.compare(meteorVOne.semver, meteorVTwo.semver);
  }

  // If their semver components are equal, then the one with the smaller wrap
  // numbers is smaller.
  return meteorVOne.wrapNum - meteorVTwo.wrapNum;
};

// Conceptually we have three types of constraints:
// 1. "compatible-with" - A@x.y.z - constraints package A to version x.y.z or
//    higher, as long as the version is backwards compatible with x.y.z.
//    "pick A compatible with x.y.z"
//    It is the default type.
// 2. "exactly" - A@=x.y.z - constraints package A only to version x.y.z and
//    nothing else.
//    "pick A exactly at x.y.z"
// 3. "any-reasonable" - "A"
//    Basically, this means any version of A ... other than ones that have
//    dashes in the version (ie, are prerelease) ... unless the prerelease
//    version has been explicitly selected (which at this stage in the game
//    means they are mentioned in a top-level constraint in the top-level
//    call to the resolver).
//
// Options:
//    removeBuildIDs:  Remove the build ID at the end of the version.
PV.parseVersionConstraint = function (versionString, options) {
  options = options || {};
  var versionDesc = { version: null, type: "any-reasonable" };

  if (!versionString) {
    return versionDesc;
  }

  if (versionString.charAt(0) === '=') {
    versionDesc.type = "exactly";
    versionString = versionString.substr(1);
  } else {
    versionDesc.type = "compatible-with";
  }

  // This will throw if the version string is invalid.
  PV.getValidServerVersion(versionString);

  if (options.removeBuildIDs) {
    versionString = PV.removeBuildID(versionString);
  }

  versionDesc.version = versionString;

  return versionDesc;
};

// Check to see if the versionString that we pass in is a valid meteor version.
//
// Returns a valid meteor version string that can be included in the
// server. That means that it has everything EXCEPT the build id. Throws if the
// entered string was invalid.
PV.getValidServerVersion = function (meteorVersionString) {

  // Strip out the wrapper num, if present and check that it is valid.
  var version = extractSemverPart(meteorVersionString);

  var versionString = version.semver;
  // NPM's semver spec supports things like 'v1.0.0' and considers them valid,
  // but we don't. Everything before the + or - should be of the x.x.x form.
  var mainVersion = versionString.split('+')[0].split('-')[0];
  if (! /^\d+\.\d+\.\d+$/.test(mainVersion)) {
      throwVersionParserError(
        "Version string must look like semver (eg '1.2.3'), not '"
          + versionString + "'.");
  };

  var cleanVersion = semver.valid(versionString);
  if (! cleanVersion ) {
    throwVersionParserError(
      "Version string must look like semver (eg '1.2.3'), not '"
        + versionString + "'.");
  }

  if (version.wrapNum) {
    cleanVersion = cleanVersion + "_" + version.wrapNum;
  }

  return cleanVersion;
};

PV.parseConstraint = function (constraintString, options) {
  if (typeof constraintString !== "string")
    throw new TypeError("constraintString must be a string");
  options = options || {};

  var splitted = constraintString.split('@');

  var name = splitted[0];
  var versionString = splitted[1] || '';

  if (splitted.length > 2) {
    // throw error complaining about @
    PV.validatePackageName('a@');
  }

  PV.validatePackageName(name);

  if (splitted.length === 2 && !versionString) {
    throwVersionParserError(
      "Version constraint for package '" + name +
        "' cannot be empty; leave off the @ if you don't want to constrain " +
        "the version.");
  }

  var constraint = {
    name: name
  };

  // Before we parse through versionString, we save it for future output.
  constraint.constraintString = versionString;

  // If we did not specify a version string, then our only constraint is
  // any-reasonable, so we are going to return that.
  if (!versionString) {
    constraint.constraints =
      [ { version: null, type: "any-reasonable" } ];
    return constraint;
  }

  // Let's parse out the versionString.
  var versionConstraints = versionString.split(/ *\|\| */);
  constraint.constraints = [];
  __.each(versionConstraints, function (versionCon) {
    constraint.constraints.push(
      PV.parseVersionConstraint(versionCon, options));
  });

  return constraint;
};

PV.validatePackageName = function (packageName, options) {
  options = options || {};

  var badChar = packageName.match(/[^a-z0-9:.\-]/);
  if (badChar) {
    if (options.detailedColonExplanation) {
      throwVersionParserError(
        "Bad character in package name: " + JSON.stringify(badChar[0]) +
          ".\n\nPackage names can only contain lowercase ASCII alphanumerics, " +
          "dash, or dot.\nIf you plan to publish a package, it must be " +
          "prefixed with your\nMeteor Developer Account username and a colon.");
    }
    throwVersionParserError(
      "Package names can only contain lowercase ASCII alphanumerics, dash, " +
        "dot, or colon, not " + JSON.stringify(badChar[0]) + ".");
  }
  if (!/[a-z]/.test(packageName)) {
    throwVersionParserError("Package names must contain a lowercase ASCII letter.");
  }
  if (packageName[0] === '.') {
    throwVersionParserError("Package names may not begin with a dot.");
  }
};

var throwVersionParserError = function (message) {
  var e = new Error(message);
  e.versionParserError = true;
  throw e;
};

PV.constraintToFullString = function (parsedConstraint) {
  return parsedConstraint.name + "@" + parsedConstraint.constraintString;
};

// Return true if the version constraint was invalid prior to 0.9.3
// (adding _ and || support)
//
// NOTE: this is not used on the client yet. This package is used by the
// package server to determine what is valid.
PV.invalidFirstFormatConstraint = function (validConstraint) {
  if (!validConstraint) return false;
  // We can check this easily right now, because we introduced some new
  // characters. Anything with those characters is invalid prior to
  // 0.9.3. XXX: If we ever have to go through these, we should write a more
  // complicated regex.
  return (/_/.test(validConstraint) ||
          /\|/.test(validConstraint));
};

// Remove a suffix like "+local" if present.
PV.removeBuildID = function (versionString) {
  return versionString.replace(/\+.*$/, '');
};


# autoupdate

This package is the heart of Meteor's Hot Code Push functionality. It
has a client component and a server component component. The client
component uses a DDP API provided by the server to subscribe to the
version ID of the most recent build of the app's client. When it sees
that a new version is available, it uses the
[reload](https://atmospherejs.com/meteor/reload) package to gracefully
save the app's state and reload it in place.

`autoupdate` is part of the [Webapp](https://www.meteor.com/webapp)
project.


var VALID_CHARACTERS = "-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

var result = VALID_CHARACTERS.indexOf(ch);

throw new Error("Unexpected character in prerelease identifier: " + ch);

keep keep keep replace keep keep keep keep keep


#!/bin/bash

# import all the environment
source $(dirname $0)/common_env.sh

if [ -z "$USE_GLOBAL_ADK" ] ; then
  exec "${ANDROID_BUNDLE}/android-sdk/tools/android" "$@"
else
  # android should be in global path
  exec android "$@"
fi


#splash_outer {
    line-height: 1.3;
    font-size: 16px;

    .mask {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        z-index: 10;
        background: black;
        opacity: 0.4;
        -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
        -webkit-transition: opacity .25s;
    }

    .dialog {
        position: fixed;
        float: left;
        top: 10%;
        left: 50%;
        width: 450px;
        height: 300px;
        margin-left: -225px;

        z-index: 11;
        background: white;
        border-width: 5px;
        border-color: black;
        border-style: solid;
        padding: 20px;

        .rounded-corners(10px);
    }

    h1 {
        font-size: 3em;
        margin: 0;
        text-align: center;
        border-style: none none solid none;
        border-color: black;
        border-width: 3px;
        margin-bottom: 30px;
        line-height: 1;
    }

    em {
        font-style: normal;
        font-weight: bold;
    }

    .hello {
        font-weight: bold;
        background-color: yellow;
    }

    ul {
        padding-left: 30px;
        margin: 30px 0px;
    }

    li {
        margin-bottom: 10px;
    }

    .beg {
        font-size: 1.3em;
        padding-bottom: 20px;
    }

    .submit {
        float: right;
        .rounded-corners(3px);
        padding: 4px;
        .buttonify(210);
    }
}

.rounded-corners (@radius: 5px) {
    border-radius: @radius;
    -webkit-border-radius: @radius;
    -moz-border-radius: @radius;
}

/*
  Handy button code adapted from:
  http://forrst.com/posts/Easy_parametric_CSS_buttons_using_LESS_HSLa-sgy
*/
.buttonify (@hue: 150, @sat: 26%, @light: 45%) {
    /* vars defined inside mixin so we can pass in the @hue param */
    @button_bg: hsla(@hue, @sat, @light, 1);
    @button_bg_light: spin(saturate(lighten(@button_bg, 5%), -10%), 22);
    @button_border: spin(saturate(lighten(@button_bg, 19%), -10%), 22);
    @button_hover: spin(lighten(@button_bg, 3%), 10);
    @button_active: spin(saturate(lighten(@button_bg, -15%), 5%), 18);

    border-top: 1px solid @button_border;
    background: @button_bg;
    background: -webkit-gradient(linear, left top, left bottom, from(@button_bg_light), to(@button_bg));
    background: -moz-linear-gradient(top, @button_bg_light, @button_bg);
    -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0;
    -moz-box-shadow: rgba(0,0,0,1) 0 1px 0;
    box-shadow: rgba(0,0,0,1) 0 1px 0;
    text-shadow: rgba(0,0,0,.4) 0 1px 0;
    color: white;
    font-size: 16px;
    text-decoration: none;
    vertical-align: middle;

    &:hover {
        border-top-color: @button_hover;
        background: @button_hover;
        color: white;
    }

    &:active {
        border-top-color: @button_active;
        background: @button_active;
        position: relative;
        top: 1px;
        color: white;
    }
}


throw new Error("Unexpected prerelease identifier part: " + part + " of type " + typeof part);

// 3000 > 101 + VALID_CHARACTERS.length *

// VALID_CHARACTERS.length. And there's a test to verify this

// ("test the edges of `versionMagnitude`")

// 4100 > 101 + VALID_CHARACTERS.length *


Weibo = {};

OAuth.registerService('weibo', 2, null, function(query) {

  var response = getTokenResponse(query);
  var uid = parseInt(response.uid, 10);

  // different parts of weibo's api seem to expect numbers, or strings
  // for uid. let's make sure they're both the same.
  if (response.uid !== uid + "")
    throw new Error("Expected 'uid' to parse to an integer: " + JSON.stringify(response));

  var identity = getIdentity(response.access_token, uid);

  return {
    serviceData: {
      // We used to store this as a string, so keep it this way rather than
      // add complexity to Account.updateOrCreateUserFromExternalService or
      // force a database migration
      id: uid + "",
      accessToken: response.access_token,
      screenName: identity.screen_name,
      expiresAt: (+new Date) + (1000 * response.expires_in)
    },
    options: {profile: {name: identity.screen_name}}
  };
});

// return an object containining:
// - uid
// - access_token
// - expires_in: lifetime of this token in seconds (5 years(!) right now)
var getTokenResponse = function (query) {
  var config = ServiceConfiguration.configurations.findOne({service: 'weibo'});
  if (!config)
    throw new ServiceConfiguration.ConfigError();

  var response;
  try {
    response = HTTP.post(
      "https://api.weibo.com/oauth2/access_token", {params: {
        code: query.code,
        client_id: config.clientId,
        client_secret: OAuth.openSecret(config.secret),
        redirect_uri: OAuth._redirectUri('weibo', config, null, {replaceLocalhost: true}),
        grant_type: 'authorization_code'
      }});
  } catch (err) {
    throw _.extend(new Error("Failed to complete OAuth handshake with Weibo. " + err.message),
                   {response: err.response});
  }

  // result.headers["content-type"] is 'text/plain;charset=UTF-8', so
  // the http package doesn't automatically populate result.data
  response.data = JSON.parse(response.content);

  if (response.data.error) { // if the http response was a json object with an error attribute
    throw new Error("Failed to complete OAuth handshake with Weibo. " + response.data.error);
  } else {
    return response.data;
  }
};

var getIdentity = function (accessToken, userId) {
  try {
    return HTTP.get(
      "https://api.weibo.com/2/users/show.json",
      {params: {access_token: accessToken, uid: userId}}).data;
  } catch (err) {
    throw _.extend(new Error("Failed to fetch identity from Weibo. " + err.message),
                   {response: err.response});
  }
};

Weibo.retrieveCredential = function(credentialToken, credentialSecret) {
  return OAuth.retrieveCredential(credentialToken, credentialSecret);
};


// Variable used in stylus_test.styl
importDashy = dashed

.stylus-overwrite-color
  font-size: 20px !important


return memo + digit / Math.pow(3000, index+1);

// Takes in two meteor versions. Returns true if the first one is less than the second.

return memo + digit / Math.pow(4100, index+1);

var currentTest = null;

var t = function (versionString, expected, descr) {
  currentTest.equal(
    _.omit(PackageVersion.parseConstraint(versionString),
           'constraintString'),
    expected,
    descr);
};

var FAIL = function (versionString) {
  currentTest.throws(function () {
    PackageVersion.parseConstraint(versionString);
  });
};

Tinytest.add("Smart Package version string parsing - old format", function (test) {
  currentTest = test;

  t("foo", { name: "foo", constraints: [{
        version: null, type: "any-reasonable" } ]});
  t("foo-1234", { name: "foo-1234", constraints: [{
        version: null, type: "any-reasonable" } ]});
  FAIL("my_awesome_InconsitentPackage123");
});

Tinytest.add("Smart Package version string parsing - compatible version, compatible-with", function (test) {
  currentTest = test;

  t("foo@1.2.3", { name: "foo", constraints: [{
        version: "1.2.3", type: "compatible-with" } ]});
  t("foo-1233@1.2.3", { name: "foo-1233", constraints: [{
        version: "1.2.3", type: "compatible-with" } ]});
  t("foo-bar@3.2.1", { name: "foo-bar", constraints: [{
        version: "3.2.1", type: "compatible-with" } ]});
  FAIL("42@0.2.0");
  FAIL("foo@1.2.3.4");
  FAIL("foo@1.4");
  FAIL("foo@1");
  FAIL("foo@");
  FAIL("foo@@");
  FAIL("foo@x.y.z");
  FAIL("foo@<1.2");
  FAIL("foo<1.2");
  FAIL("foo@1.2.3_abc");
  FAIL("foo@1.2.3+1234_1");
  FAIL("foo@1.2.3_1-rc1");
  FAIL("foo-1233@1.2.3_0");
  FAIL("foo-1233@1.2.3_");
  FAIL("foo-1233@1.2.3_0123");

  t("foo@1.2.3_1", { name: "foo", constraints: [{
       version: "1.2.3_1", type: "compatible-with" } ]});
  t("foo-bar@3.2.1-rc0_123", { name: "foo-bar", constraints: [{
       version: "3.2.1-rc0_123", type: "compatible-with" } ]});
  t("foo-1233@1.2.3_5+1234", { name: "foo-1233", constraints: [{
       version: "1.2.3_5+1234", type: "compatible-with" } ]});
  t("foo", { name: "foo", constraints: [{
       version: null, type: "any-reasonable" } ]});
});

Tinytest.add("Smart Package version string parsing - compatible version, exactly", function (test) {
  currentTest = test;

  t("foo@=1.2.3", { name: "foo", constraints: [
         { version: "1.2.3", type: "exactly" } ]});
  t("foo-bar@=3.2.1", { name: "foo-bar", constraints: [{
      version: "3.2.1", type: "exactly" } ]});
  t("foo@=1.2.3_1", { name: "foo", constraints: [{
       version: "1.2.3_1", type: "exactly" } ]});
  t("foo-bar@=3.2.1_34", { name: "foo-bar", constraints: [{
       version: "3.2.1_34", type: "exactly" } ]});

  FAIL("42@=0.2.0");
  FAIL("foo@=1.2.3.4");
  FAIL("foo@=1.4");
  FAIL("foo@=1");
  FAIL("foo@@=");
  FAIL("foo@=@");
  FAIL("foo@=x.y.z");
  FAIL("foo@=<1.2");
  FAIL("foo@<=1.2");
  FAIL("foo<=1.2");
  FAIL("foo@=1.2.3_rc0");

  // We no longer support @>=.
  FAIL("foo@>=1.2.3");
  FAIL("foo-bar@>=3.2.1");
  FAIL("42@>=0.2.0");
  FAIL("foo@>=1.2.3.4");
  FAIL("foo@>=1.4");
  FAIL("foo@>=1");
  FAIL("foo@@>=");
  FAIL("foo@>=@");
  FAIL("foo@>=x.y.z");
  FAIL("foo@=>12.3.11");
});

Tinytest.add("Smart Package version string parsing - or", function (test) {
  currentTest = test;

  t("foo@1.0.0 || 2.0.0 || 3.0.0 || =4.0.0-rc1",
    { name: "foo", constraints:
      [{ version: "1.0.0", type: "compatible-with"},
       { version: "2.0.0", type: "compatible-with"},
       { version: "3.0.0", type: "compatible-with"},
       { version: "4.0.0-rc1", type: "exactly"}]
   });
  t("foo@1.0.0|| 2.0.0||3.0.0    ||     =4.0.0-rc1",
    { name: "foo", constraints:
      [{ version: "1.0.0", type: "compatible-with"},
       { version: "2.0.0", type: "compatible-with"},
       { version: "3.0.0", type: "compatible-with"},
       { version: "4.0.0-rc1", type: "exactly"}]
   });
  t("foo-bar@=3.2.1 || 1.0.0",
    { name: "foo-bar", constraints:
      [{ version: "3.2.1", type: "exactly"},
       { version: "1.0.0", type: "compatible-with"}]
   });
  t("foo@=1.2.3_1 || 1.2.4",
    { name: "foo", constraints:
      [{ version: "1.2.3_1", type: "exactly"},
       { version: "1.2.4", type: "compatible-with"}]
   });
  t("foo-bar@=3.2.1_34 || =3.2.1-rc1",
    { name: "foo-bar", constraints:
      [{ version: "3.2.1_34", type: "exactly"},
       { version: "3.2.1-rc1", type: "exactly"}]
    });

  FAIL("foo@1.0.0 1.0.0");
  FAIL("foo@1.0.0 | 1.0.0");
  FAIL("foo || bar");
  FAIL("foo@1.0.0-rc|1.0.0");

  // This is the current implementation, but is arguably not great.
  FAIL("foo@1.0.0 "); // trailing space
});

Tinytest.add(
  "Meteor Version string parsing - less than, compare, version magnitude",
  function (test) {
    var compare = function (v1, v2, expected) {
      if (expected === '<') {
        test.isTrue(PackageVersion.lessThan(v1, v2));
        test.isTrue(PackageVersion.versionMagnitude(v1) < PackageVersion.versionMagnitude(v2));
        test.isTrue(PackageVersion.compare(v1, v2) < 0);
      } else if (expected === '=') {
        test.isFalse(PackageVersion.lessThan(v1, v2));
        test.isFalse(PackageVersion.lessThan(v2, v1));
        test.isTrue(PackageVersion.versionMagnitude(v1) === PackageVersion.versionMagnitude(v2));
        test.isTrue(PackageVersion.compare(v1, v2) === 0);
      } else if (expected === '>') {
        test.isTrue(PackageVersion.lessThan(v2, v1));
        test.isTrue(PackageVersion.versionMagnitude(v1) > PackageVersion.versionMagnitude(v2));
        test.isTrue(PackageVersion.compare(v1, v2) > 0);
      } else {
        throw new Error("expected should be '<', '=' or '>'");
      }
    };

    compare("1.0.0", "1.2.0", "<");
    compare("1.0.0_50", "1.0.1", "<");
    compare("1.0.0_50", "1.2.0", "<");
    compare("1.0.0_1", "1.0.0_2", "<");
    compare("1.0.0_2", "1.0.0_10", "<"); // verify that we compare _N "wrap numbers" as numbers, not strings
    compare("1.0.0", "1.0.0_2", "<");
    compare("1.99.0_99", "3.0.0_2", "<");
    compare("1.99.0", "2.0.0", "<");
    compare("1.0.0_5", "1.0.0_2", ">");
    compare("1.0.0_99", "1.2.0", "<");
    compare("1.0.0_99", "1.0.1", "<");
    compare("1.0.0_1", "1.0.0_2", "<");
    compare("1.0.0", "1.0.0_2", "<");
    compare("1.99.0_99", "3.0.0_2", "<");

    compare("1.0.0_5", "1.0.0_2", ">");
    compare("1.0.0", "1.0.0", "=");
    compare("1.0.0_5", "1.0.0_5", "=");
    compare("1.2.0", "1.0.0", ">");
    compare("1.0.1", "1.0.0_5", ">");

    // Rule 11 from http://semver.org
    compare("0.99.99", "1.0.0-alpha.1", "<");
    compare("1.0.0-alpha", "1.0.0-alpha.1", "<");
    compare("1.0.0-alpha.1", "1.0.0-alpha.beta", "<");
    compare("1.0.0-alpha.beta", "1.0.0-beta", "<");
    compare("1.0.0-beta", "1.0.0-beta.2", "<");
    compare("1.0.0-beta.2", "1.0.0-beta.11", "<");
    compare("1.0.0-beta.11", "1.0.0-rc.1", "<");
    compare("1.0.0-rc.1", "1.0.0", "<");

    // dashes are allowed in prerelease parts
    compare("1.0.0--alpha", "1.0.0-alpha", "<");
    compare("1.0.0-a-lpha", "1.0.0-alpha", "<");
    // test single character prerelease parts
    compare("1.0.0-r.1", "1.0.0", "<");
    // test the edges of `versionMagnitude`
    compare("1.0.0-zzzzzzzzzzzz", "1.0.0", "<");

    // Our broken implementation of Rule 11 (see [*] above the
    // declaration of PackageVersion.versionMagnitude). Maybe one day
    // we'll fix it, in which case replace "===" with ">"
    test.isTrue(PackageVersion.versionMagnitude("1.0.0-beta.0") ===
                PackageVersion.versionMagnitude("1.0.0-bear.0"));

  });

Tinytest.add("Invalid in 0.9.2", function (test) {
  // Note that invalidFirstFormatConstraint assumes that the initial version
  // passed in has been previously checked to be valid in 0.9.3.

  // These are invalid in 0.9.2, but valid in 0.9.3 and above.
  var invalidVersions =
    ["1.0.0_1", "1.0.0 || 2.0.0", "1.0.0-rc1_1",
     "3.4.0-rc1 || =1.0.0"];
  _.each(invalidVersions, function (v) {
    test.isTrue(PackageVersion.invalidFirstFormatConstraint(v));
  });

  // These are all valid in 0.9.2.
  var validVersions =
    ["1.0.0", "2.0.0-rc1", "=2.5.0"];
  _.each(validVersions, function (v) {
    test.isFalse(PackageVersion.invalidFirstFormatConstraint(v));
  });
});